Skip to content

Commit 0cd049e

Browse files
authored
Auto populate IDOM component registry (#24)
* Auto populate IDOM registered components * avoid multiple component registrations * add component regex tests
1 parent 9336098 commit 0cd049e

File tree

7 files changed

+222
-79
lines changed

7 files changed

+222
-79
lines changed

src/django_idom/apps.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.apps import AppConfig
2+
3+
from django_idom.utils import ComponentPreloader
4+
5+
6+
class DjangoIdomConfig(AppConfig):
7+
name = "django_idom"
8+
9+
def ready(self):
10+
# Populate the IDOM component registry when Django is ready
11+
ComponentPreloader().register_all()

src/django_idom/templatetags/idom.py

+1-26
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import json
2-
import sys
3-
from importlib import import_module
42
from urllib.parse import urlencode
53
from uuid import uuid4
64

75
from django import template
86

97
from django_idom.config import (
10-
IDOM_REGISTERED_COMPONENTS,
118
IDOM_WEB_MODULES_URL,
129
IDOM_WEBSOCKET_URL,
1310
IDOM_WS_MAX_RECONNECT_DELAY,
1411
)
12+
from django_idom.utils import _register_component
1513

1614

1715
register = template.Library()
@@ -33,26 +31,3 @@ def idom_component(_component_id_, **kwargs):
3331
"idom_component_id": _component_id_,
3432
"idom_component_params": urlencode({"kwargs": json_kwargs}),
3533
}
36-
37-
38-
def _register_component(full_component_name: str) -> None:
39-
module_name, component_name = full_component_name.rsplit(".", 1)
40-
41-
if module_name in sys.modules:
42-
module = sys.modules[module_name]
43-
else:
44-
try:
45-
module = import_module(module_name)
46-
except ImportError as error:
47-
raise RuntimeError(
48-
f"Failed to import {module_name!r} while loading {component_name!r}"
49-
) from error
50-
51-
try:
52-
component = getattr(module, component_name)
53-
except AttributeError as error:
54-
raise RuntimeError(
55-
f"Module {module_name!r} has no component named {component_name!r}"
56-
) from error
57-
58-
IDOM_REGISTERED_COMPONENTS[full_component_name] = component

src/django_idom/utils.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import logging
2+
import os
3+
import re
4+
from fnmatch import fnmatch
5+
from importlib import import_module
6+
from typing import Set
7+
8+
from django.template import engines
9+
from django.utils.encoding import smart_str
10+
11+
from django_idom.config import IDOM_REGISTERED_COMPONENTS
12+
13+
14+
COMPONENT_REGEX = re.compile(r"{% *idom_component ((\"[^\"']*\")|('[^\"']*')).*?%}")
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
def _register_component(full_component_name: str) -> None:
19+
if full_component_name in IDOM_REGISTERED_COMPONENTS:
20+
return
21+
22+
module_name, component_name = full_component_name.rsplit(".", 1)
23+
24+
try:
25+
module = import_module(module_name)
26+
except ImportError as error:
27+
raise RuntimeError(
28+
f"Failed to import {module_name!r} while loading {component_name!r}"
29+
) from error
30+
31+
try:
32+
component = getattr(module, component_name)
33+
except AttributeError as error:
34+
raise RuntimeError(
35+
f"Module {module_name!r} has no component named {component_name!r}"
36+
) from error
37+
38+
IDOM_REGISTERED_COMPONENTS[full_component_name] = component
39+
40+
41+
class ComponentPreloader:
42+
def register_all(self):
43+
"""Registers all IDOM components found within Django templates."""
44+
# Get all template folder paths
45+
paths = self._get_paths()
46+
# Get all HTML template files
47+
templates = self._get_templates(paths)
48+
# Get all components
49+
components = self._get_components(templates)
50+
# Register all components
51+
self._register_components(components)
52+
53+
def _get_loaders(self):
54+
"""Obtains currently configured template loaders."""
55+
template_source_loaders = []
56+
for e in engines.all():
57+
if hasattr(e, "engine"):
58+
template_source_loaders.extend(
59+
e.engine.get_template_loaders(e.engine.loaders)
60+
)
61+
loaders = []
62+
for loader in template_source_loaders:
63+
if hasattr(loader, "loaders"):
64+
loaders.extend(loader.loaders)
65+
else:
66+
loaders.append(loader)
67+
return loaders
68+
69+
def _get_paths(self) -> Set:
70+
"""Obtains a set of all template directories."""
71+
paths = set()
72+
for loader in self._get_loaders():
73+
try:
74+
module = import_module(loader.__module__)
75+
get_template_sources = getattr(module, "get_template_sources", None)
76+
if get_template_sources is None:
77+
get_template_sources = loader.get_template_sources
78+
paths.update(smart_str(origin) for origin in get_template_sources(""))
79+
except (ImportError, AttributeError, TypeError):
80+
pass
81+
82+
return paths
83+
84+
def _get_templates(self, paths: Set) -> Set:
85+
"""Obtains a set of all HTML template paths."""
86+
extensions = [".html"]
87+
templates = set()
88+
for path in paths:
89+
for root, dirs, files in os.walk(path, followlinks=False):
90+
templates.update(
91+
os.path.join(root, name)
92+
for name in files
93+
if not name.startswith(".")
94+
and any(fnmatch(name, "*%s" % glob) for glob in extensions)
95+
)
96+
97+
return templates
98+
99+
def _get_components(self, templates: Set) -> Set:
100+
"""Obtains a set of all IDOM components by parsing HTML templates."""
101+
components = set()
102+
for template in templates:
103+
try:
104+
with open(template, "r", encoding="utf-8") as template_file:
105+
match = COMPONENT_REGEX.findall(template_file.read())
106+
if not match:
107+
continue
108+
components.update(
109+
[group[0].replace('"', "").replace("'", "") for group in match]
110+
)
111+
except Exception:
112+
pass
113+
114+
return components
115+
116+
def _register_components(self, components: Set) -> None:
117+
"""Registers all IDOM components in an iterable."""
118+
for component in components:
119+
try:
120+
_register_component(component)
121+
_logger.info("IDOM has registered component %s", component)
122+
except Exception:
123+
_logger.warning("IDOM failed to register component %s", component)

tests/test_app/tests.py

-53
This file was deleted.

tests/test_app/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import * # noqa: F401, F403
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import sys
3+
4+
from channels.testing import ChannelsLiveServerTestCase
5+
from selenium import webdriver
6+
from selenium.webdriver.common.by import By
7+
from selenium.webdriver.support import expected_conditions
8+
from selenium.webdriver.support.ui import WebDriverWait
9+
10+
11+
# These tests are broken on Windows due to Selenium
12+
if sys.platform != "win32":
13+
14+
class TestIdomCapabilities(ChannelsLiveServerTestCase):
15+
def setUp(self):
16+
self.driver = make_driver(5, 5)
17+
self.driver.get(self.live_server_url)
18+
19+
def tearDown(self) -> None:
20+
self.driver.quit()
21+
22+
def wait(self, timeout=10):
23+
return WebDriverWait(self.driver, timeout)
24+
25+
def wait_until(self, condition, timeout=10):
26+
return self.wait(timeout).until(lambda driver: condition())
27+
28+
def test_hello_world(self):
29+
self.driver.find_element_by_id("hello-world")
30+
31+
def test_counter(self):
32+
button = self.driver.find_element_by_id("counter-inc")
33+
count = self.driver.find_element_by_id("counter-num")
34+
35+
for i in range(5):
36+
self.wait_until(lambda: count.get_attribute("data-count") == str(i))
37+
button.click()
38+
39+
def test_parametrized_component(self):
40+
element = self.driver.find_element_by_id("parametrized-component")
41+
self.assertEqual(element.get_attribute("data-value"), "579")
42+
43+
def test_component_from_web_module(self):
44+
self.wait(20).until(
45+
expected_conditions.visibility_of_element_located(
46+
(By.CLASS_NAME, "VictoryContainer")
47+
)
48+
)
49+
50+
51+
def make_driver(page_load_timeout, implicit_wait_timeout):
52+
options = webdriver.ChromeOptions()
53+
options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0)))
54+
driver = webdriver.Chrome(options=options)
55+
driver.set_page_load_timeout(page_load_timeout)
56+
driver.implicitly_wait(implicit_wait_timeout)
57+
return driver

tests/test_app/tests/test_regex.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.test import TestCase
2+
3+
from django_idom.utils import COMPONENT_REGEX
4+
5+
6+
class RegexTests(TestCase):
7+
def test_component_regex(self):
8+
for component in {
9+
r'{%idom_component "my.component"%}',
10+
r"{%idom_component 'my.component'%}",
11+
r'{% idom_component "my.component" %}',
12+
r"{% idom_component 'my.component' %}",
13+
r'{% idom_component "my.component" class="my_thing" %}',
14+
r'{% idom_component "my.component" class="my_thing" attr="attribute" %}',
15+
}:
16+
self.assertRegex(component, COMPONENT_REGEX)
17+
18+
for fake_component in {
19+
r'{% not_a_real_thing "my.component" %}',
20+
r"{% idom_component my.component %}",
21+
r"""{% idom_component 'my.component" %}""",
22+
r'{ idom_component "my.component" }',
23+
r'{{ idom_component "my.component" }}',
24+
r"idom_component",
25+
r"{%%}",
26+
r" ",
27+
r"",
28+
}:
29+
self.assertNotRegex(fake_component, COMPONENT_REGEX)

0 commit comments

Comments
 (0)