Skip to content

Commit 9aa33bd

Browse files
authored
Fix generator component ordering (#20)
* Various changes: - Add templates_location config option - Add require_component_types config option - Add test for template type generator - Fix issue with template type generator not outputting components in correct order for class inheritence - Fix whitespacing and template layout in core component template * Add PR tests
1 parent 3357d4a commit 9aa33bd

5 files changed

Lines changed: 215 additions & 38 deletions

File tree

.github/workflows/pr.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Run pull request tasks
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
run_tests:
8+
name: Run tests
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v5
13+
with:
14+
fetch-depth: 0
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v6
18+
with:
19+
python-version: '3.13'
20+
21+
- name: Install build tools
22+
run: pip install .[build]
23+
24+
- name: Check build version
25+
run: python3 -m setuptools_scm
26+
27+
- name: Build wheel
28+
run: python -m build
29+
30+
- name: Check wheel
31+
run: |
32+
pip install twine
33+
twine check dist/*
34+
35+
- name: Install built wheel
36+
run: pip install dist/*.whl
37+
38+
- name: Install maintainer requirements for testing
39+
run: pip install .[maintainer]
40+
41+
- name: Run tests
42+
run: pytest

volt/components.py.j2

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,58 @@
33
This file is automatically generated.
44
Do not edit this file directly.
55
"""
6-
{% if not import_types %}from typing import Any
6+
{% if not import_types %}
7+
from typing import Any
78
{% endif %}
89
from dataclasses import dataclass
910

1011
from volt.components import Component
12+
1113
{% if components and import_types %}
12-
from custom_types import ({% for component in components %}{% if component.fields %}
13-
{{ component.name }}Types,{% endif %}{% endfor %}
14+
from custom_types import (
15+
{% for component in components %}
16+
{% if component.fields %}
17+
{{ component.name }}Types,
18+
{% endif %}
19+
{% endfor %}
1420
)
1521
{% endif %}
22+
1623
{% for component in components %}
17-
{% if component.parent_components|length == 0 %}class {{ component.name }}(Component):
18-
{% elif component.parent_components|length == 1 %}class {{ component.name }}({{ component.parent_components[0] }}):
19-
{% else %}class {{ component.name }}(
20-
{% for parent in component.parent_components %}{{ parent }},
24+
25+
{% if component.parent_components|length == 0 %}
26+
class {{ component.name }}(Component):
27+
{% elif component.parent_components|length == 1 %}
28+
class {{ component.name }}({{ component.parent_components[0] }}):
29+
{% else %}
30+
class {{ component.name }}(
31+
{% for parent in component.parent_components %}
32+
{{ parent }},
2133
{% endfor %}
22-
):{% endif %}
34+
):
35+
{% endif %}
2336
template_name: str = "{{ component.template_name }}"
2437
block_name: str = "{{ component.block_name }}"
2538

26-
{% if component.parent_components|length == 0 %}@dataclass
27-
class Context(Component.Context):{% elif component.parent_components|length == 1 %}@dataclass
28-
class Context({{ component.parent_components[0] }}.Context):{% else %}@dataclass
39+
@dataclass
40+
{% if component.parent_components|length == 0 %}
41+
class Context(Component.Context):
42+
{% elif component.parent_components|length == 1 %}
43+
class Context({{ component.parent_components[0] }}.Context):
44+
{% else %}
2945
class Context(
30-
{% for parent in component.parent_components %}{{ parent }}.Context,
46+
{% for parent in component.parent_components %}
47+
{{ parent }}.Context,
3148
{% endfor %}
32-
):{% endif %}
33-
{% for field in component.fields %}{{ field }}: {% if import_types %}{{ component.name }}Types.{{ field }}
34-
{% else %}Any{% endif %}{% else %}...
49+
):
50+
{% endif %}
51+
{% for field in component.fields %}
52+
{{ field }}: {% if import_types %}{{ component.name }}Types.{{ field }}{% else %}Any{% endif %}
53+
54+
{% else %}
55+
...
3556
{% endfor %}
57+
3658
def __init__(self, context: Context) -> None:
3759
super().__init__(context)
3860

volt/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,9 @@ def get_config_value(name: str, default: T) -> T:
7575

7676
allowed_hosts = get_config_value("allowed_hosts", default=[])
7777
log.debug("allowed_hosts: %s", allowed_hosts)
78+
79+
templates_location = get_config_value("templates_location", default="templates")
80+
log.debug("templates_location: %s", templates_location)
81+
82+
require_component_types = get_config_value("require_component_types", default=False)
83+
log.debug("require_component_types: %s", require_component_types)

volt/generator.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
from jinja2 import Environment, FileSystemLoader, meta
66
from jinja2.nodes import Block, For, Name, Tuple
77

8-
log = logging.getLogger('volt.generator.py')
9-
log.setLevel(logging.DEBUG)
8+
from volt import config
109

11-
environment = Environment(loader=FileSystemLoader("templates/"))
10+
log = logging.getLogger("volt.generator.py")
1211

1312

1413
@dataclass
@@ -29,7 +28,9 @@ class Context:
2928
all_components: list[GeneratedComponent] = []
3029

3130

32-
def get_block_children(block: Block, template_name: str, referenced_templates: Iterator[str | None], top_level: bool) -> Iterable[Block]:
31+
def get_block_children(
32+
block: Block, template_name: str, referenced_templates: Iterator[str | None], top_level: bool
33+
) -> Iterable[Block]:
3334
blocks: list[Block] = []
3435

3536
child_blocks = block.iter_child_nodes()
@@ -40,7 +41,7 @@ def get_block_children(block: Block, template_name: str, referenced_templates: I
4041
blocks.append(child_block)
4142
blocks.extend(get_block_children(child_block, template_name, referenced_templates, top_level=False))
4243

43-
# We want to make sure that we are excluding fields that are captured by parents. Since we are inheriting from
44+
# We want to make sure that we are excluding fields that are captured by parents. Since we are inheriting from
4445
# these components, we don't want to require them on on the child components as well
4546
parent_fields: list[str] = []
4647
for parent_block in blocks:
@@ -52,13 +53,10 @@ def get_block_children(block: Block, template_name: str, referenced_templates: I
5253
# in which case we name it the file name
5354
formatted_template_name = template_name_as_title(template_name)
5455
name = formatted_template_name + name_as_title(block.name)
55-
# if block.name == "content":
56-
# name = template_name[: template_name.find(".")].title()
57-
5856

5957
parent_components = [formatted_template_name + name_as_title(block.name) for block in blocks]
6058
# If we have an 'extends' at the top level, we ensure this is added as a parent component to any component
61-
# at that same 'top level', so as to ensure any fields in extended templates are also required as part of the
59+
# at that same 'top level', so as to ensure any fields in extended templates are also required as part of the
6260
# dataclass context
6361
if top_level:
6462
for referenced_template in referenced_templates:
@@ -82,6 +80,7 @@ def name_as_title(name: str) -> str:
8280
name = f"{name.replace('_', ' ').title().replace(' ', '')}"
8381
return name
8482

83+
8584
def template_name_as_title(template_name: str) -> str:
8685
return template_name[: template_name.find(".")].title().replace("_", "")
8786

@@ -126,11 +125,12 @@ def get_block_fields(block: Block) -> list[str]:
126125

127126
return fields
128127

128+
129129
# TODO: Check against templates without blocks
130-
def generate():
130+
def _generate(environment: Environment, import_types: bool) -> str:
131131
context = Context(
132132
components=[],
133-
import_types=True,
133+
import_types=import_types,
134134
)
135135

136136
if environment.loader is None:
@@ -155,28 +155,50 @@ def generate():
155155
parent_components.append(template_name_as_title(template_name) + name_as_title(block.name))
156156

157157
name = template_name_as_title(template_name)
158-
all_components.append(GeneratedComponent(
159-
name=name,
160-
template_name=template_name,
161-
block_name="content",
162-
parent_components=parent_components,
163-
fields=[],
164-
))
158+
all_components.append(
159+
GeneratedComponent(
160+
name=name,
161+
template_name=template_name,
162+
block_name="content",
163+
parent_components=parent_components,
164+
fields=[],
165+
)
166+
)
167+
168+
# Add all components without parents
169+
for c in all_components:
170+
if len(list(c.parent_components)) != 0:
171+
continue
172+
context.components.append(c)
165173

174+
while len(all_components) != len(context.components):
175+
for c in all_components:
176+
if c in context.components:
177+
continue
178+
if not all(pc in [cc.name for cc in context.components] for pc in c.parent_components):
179+
continue
166180

167-
for component in all_components:
168-
log.warning(f"component created: {component}")
169-
context.components.append(component)
181+
context.components.append(c)
170182

171183
parent_dir = Path(__file__).parent
172-
gen_environment = Environment(loader=FileSystemLoader(parent_dir))
173-
template_file = "components.py.j2"
184+
gen_environment = Environment(loader=FileSystemLoader(parent_dir), trim_blocks=True, lstrip_blocks=True)
185+
template_file = "components.py.j2"
174186
template = gen_environment.get_template(template_file)
175187
output = template.render(asdict(context))
188+
return output
189+
176190

191+
def generate():
192+
templates_location = Path(config.templates_location)
193+
if not templates_location.is_dir():
194+
raise Exception(f"{config.templates_location} must be a directory")
195+
196+
environment = Environment(loader=FileSystemLoader(templates_location))
197+
output = _generate(environment, config.require_component_types)
177198
with open("components_gen.py", "w") as f:
178199
len_written = f.write(output)
179200
assert len_written == len(output)
180201

202+
181203
if __name__ == "__main__":
182204
generate()

volt/generator_test.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from jinja2 import DictLoader, Environment
2+
from volt.generator import _generate
3+
4+
5+
def test_generate():
6+
templates = {
7+
"about.html": """
8+
{% extends "base.html" %}
9+
10+
{% block content %}
11+
{{ bar }}
12+
{% endblock %}
13+
""",
14+
"base.html": """
15+
{{ foo }}
16+
{% block content %}
17+
{% endblock %}
18+
""",
19+
}
20+
environment = Environment(loader=DictLoader(templates))
21+
output = _generate(environment, import_types=True)
22+
print(output)
23+
expected_output = """# pyright: basic
24+
\"\"\"
25+
This file is automatically generated.
26+
Do not edit this file directly.
27+
\"\"\"
28+
from dataclasses import dataclass
29+
30+
from volt.components import Component
31+
32+
from custom_types import (
33+
AboutContentTypes,
34+
)
35+
36+
37+
class BaseContent(Component):
38+
template_name: str = "base.html"
39+
block_name: str = "content"
40+
41+
@dataclass
42+
class Context(Component.Context):
43+
...
44+
45+
def __init__(self, context: Context) -> None:
46+
super().__init__(context)
47+
48+
49+
class Base(BaseContent):
50+
template_name: str = "base.html"
51+
block_name: str = "content"
52+
53+
@dataclass
54+
class Context(BaseContent.Context):
55+
...
56+
57+
def __init__(self, context: Context) -> None:
58+
super().__init__(context)
59+
60+
61+
class AboutContent(Base):
62+
template_name: str = "about.html"
63+
block_name: str = "content"
64+
65+
@dataclass
66+
class Context(Base.Context):
67+
bar: AboutContentTypes.bar
68+
69+
def __init__(self, context: Context) -> None:
70+
super().__init__(context)
71+
72+
73+
class About(AboutContent):
74+
template_name: str = "about.html"
75+
block_name: str = "content"
76+
77+
@dataclass
78+
class Context(AboutContent.Context):
79+
...
80+
81+
def __init__(self, context: Context) -> None:
82+
super().__init__(context)
83+
84+
"""
85+
assert output == expected_output

0 commit comments

Comments
 (0)