Skip to content

Commit b2a2e97

Browse files
committed
Preserve module factory cache entries
1 parent fd4f032 commit b2a2e97

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

‎AUTHORS.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ Patches and suggestions
6363
- Ville Skyttä
6464
- Hugo van Kemenade
6565
- Mark Vasilkov
66+
- fly1d
6667

‎CHANGES.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Features:
1818

1919
Bug fixes:
2020

21+
* Keep cached tree builder modules when another configuration is requested. (#585)
2122
* The sanitizer now permits ``<summary>`` tags. It used to allow ``<details>``
2223
already. (#423)
2324

‎html5lib/_utils.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,10 @@ def moduleFactory(baseModule, *args, **kwargs):
135135
mod = ModuleType(name)
136136
objs = factory(baseModule, *args, **kwargs)
137137
mod.__dict__.update(objs)
138-
if "name" not in moduleCache:
138+
if name not in moduleCache:
139139
moduleCache[name] = {}
140-
if "args" not in moduleCache[name]:
140+
if args not in moduleCache[name]:
141141
moduleCache[name][args] = {}
142-
if "kwargs" not in moduleCache[name][args]:
143-
moduleCache[name][args][kwargs_tuple] = {}
144142
moduleCache[name][args][kwargs_tuple] = mod
145143
return mod
146144

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from __future__ import absolute_import, division, unicode_literals
2+
3+
from types import ModuleType
4+
import xml.etree.ElementTree as ElementTree
5+
6+
import pytest
7+
8+
from html5lib import HTMLParser, getTreeBuilder
9+
from html5lib._utils import moduleFactoryFactory
10+
11+
12+
def _module_factory():
13+
def factory(backend, *args, **kwargs):
14+
return {"backend": backend, "arguments": args, "options": kwargs}
15+
16+
return moduleFactoryFactory(factory)
17+
18+
19+
@pytest.mark.parametrize("args_a, options_a, args_b, options_b", [
20+
(("first",), {}, ("second",), {}),
21+
((), {"enabled": False}, (), {"enabled": True}),
22+
(("first",), {"enabled": False}, ("first",), {"enabled": True}),
23+
(("first",), {"enabled": False}, ("second",), {"enabled": False}),
24+
], ids=["positional", "keyword", "shared-arguments", "shared-options"])
25+
def test_module_factory_reuses_each_configuration(args_a, options_a, args_b, options_b):
26+
backend = ModuleType(str("factory_backend"))
27+
build = _module_factory()
28+
29+
first = build(backend, *args_a, **options_a)
30+
second = build(backend, *args_b, **options_b)
31+
first_again = build(backend, *args_a, **options_a)
32+
second_again = build(backend, *args_b, **options_b)
33+
34+
assert first.backend is backend
35+
assert second.backend is backend
36+
assert first.arguments == args_a
37+
assert second.arguments == args_b
38+
assert first.options == options_a
39+
assert second.options == options_b
40+
assert first_again.arguments == args_a
41+
assert first_again.options == options_a
42+
assert second_again.arguments == args_b
43+
assert second_again.options == options_b
44+
assert first is not second
45+
assert first_again is first
46+
assert second_again is second
47+
48+
49+
def test_module_factory_keeps_distinct_backends_cached():
50+
backend_a = ModuleType(str("factory_backend_a"))
51+
backend_b = ModuleType(str("factory_backend_b"))
52+
build = _module_factory()
53+
54+
first = build(backend_a, enabled=True)
55+
second = build(backend_b, enabled=True)
56+
57+
assert first.backend is backend_a
58+
assert second.backend is backend_b
59+
assert first is not second
60+
assert build(backend_a, enabled=True) is first
61+
assert build(backend_b, enabled=True) is second
62+
63+
64+
@pytest.mark.parametrize("full_tree", [False, True])
65+
def test_etree_builder_identity_survives_alternating_options(full_tree):
66+
first = getTreeBuilder("etree", ElementTree, fullTree=full_tree)
67+
first_parser = HTMLParser(tree=first, namespaceHTMLElements=False)
68+
second = getTreeBuilder("etree", ElementTree, fullTree=not full_tree)
69+
second_parser = HTMLParser(tree=second, namespaceHTMLElements=False)
70+
first_again = getTreeBuilder("etree", ElementTree, fullTree=full_tree)
71+
second_again = getTreeBuilder("etree", ElementTree, fullTree=not full_tree)
72+
73+
assert first.implementation is ElementTree
74+
assert second.implementation is ElementTree
75+
assert first is not second
76+
assert first_again is first
77+
assert second_again is second
78+
assert isinstance(first_parser.tree, first_again)
79+
assert isinstance(second_parser.tree, second_again)
80+
81+
# Parsing is an existing-behavior control, not the cache regression itself.
82+
parsers = [(first_parser, full_tree), (second_parser, not full_tree)]
83+
for parser, includes_document in parsers:
84+
document = parser.parse("<p>content</p>")
85+
root = document.find("html") if includes_document else document
86+
assert root.tag == "html"
87+
assert root.find("body/p").text == "content"

0 commit comments

Comments
 (0)