Skip to content

Commit 28ee8e6

Browse files
Refactor cgx package to sfc package with better names modules (#122)
1 parent 8803f79 commit 28ee8e6

15 files changed

Lines changed: 229 additions & 120 deletions

collagraph/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .component import Component # noqa: F401
55
from .renderers import * # noqa: F403
66
from .types import EventLoopType, VNode # noqa: F401
7-
from .cgx import importer # noqa: F401
7+
from .sfc import importer # noqa: F401
88

99
__version__ = version("collagraph")
1010

collagraph/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def available_renderers():
2626
def init_collagraph(
2727
renderer_type: str, component_path: Path, state: dict | None = None
2828
):
29-
component_class, _ = cg.cgx.cgx.load(component_path)
29+
component_class, _ = cg.sfc.load(component_path)
3030
props = reactive(state or {})
3131

3232
if renderer_type == "pygfx":

collagraph/__pyinstaller/hook-collagraph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from PyInstaller.utils.hooks import collect_data_files
55

6-
from collagraph.cgx.cgx import CGXParser, get_script_ast
6+
from collagraph.sfc.compiler import get_script_ast
7+
from collagraph.sfc.parser import CGXParser
78

89

910
def hook(hook_api):

collagraph/cgx/__init__.py

Whitespace-only changes.

collagraph/cgx/importer.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

collagraph/renderers/pyside/objects/standarditem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def insert(self, el, anchor=None):
2828
def remove(self, el):
2929
if hasattr(el, "model_index"):
3030
# Only support removal of rows for now
31-
row, column = getattr(el, "model_index")
31+
row, _column = getattr(el, "model_index")
3232
if model := el.model():
3333
index = model.indexFromItem(el)
3434
row = index.row()

collagraph/sfc/__init__.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from collagraph import Component
2+
3+
from .compiler import construct_ast
4+
5+
6+
def load(path, namespace=None):
7+
"""
8+
Loads and returns a component from a .cgx file.
9+
10+
A subclass of Component will be created from the .cgx file
11+
where the contents of all tags in the root will be used as
12+
the `render` function, except for the contents of the <script>
13+
tag, which will be used to provide the body of the component.
14+
15+
For example:
16+
17+
<item foo="bar">
18+
<item baz="bla"/>
19+
</item>
20+
21+
<script>
22+
import collagraph as cg
23+
24+
class Foo(cg.Component):
25+
pass
26+
</script>
27+
28+
"""
29+
template = path.read_text()
30+
31+
return load_from_string(template, path, namespace=namespace)
32+
33+
34+
def load_from_string(template, path=None, namespace=None):
35+
"""
36+
Load template from a string.
37+
Returns tuple of class definition and module namespace.
38+
"""
39+
if path is None:
40+
path = "<template>"
41+
42+
# Construct the AST tree
43+
tree, name = construct_ast(path=path, template=template)
44+
45+
# Compile the tree into a code object (module)
46+
code = compile(tree, filename=str(path), mode="exec")
47+
# Execute the code as module and pass a dictionary that will capture
48+
# the global and local scope of the module
49+
if namespace is None:
50+
namespace = {}
51+
exec(code, namespace)
52+
53+
# Check that the class definition is an actual subclass of Component
54+
component_class = namespace[name]
55+
if not issubclass(component_class, Component):
56+
raise ValueError(
57+
f"The last class defined in {path} is not a subclass of "
58+
f"Component: {component_class}"
59+
)
60+
namespace["__component_class"] = component_class
61+
namespace["__component_name"] = name
62+
return component_class, namespace
Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
from os import environ
88
from pathlib import Path
99

10-
from collagraph import Component
11-
1210
# Adjust this setting to disable some runtime checks
1311
# Defaults to True, except when it is part of an installed application
1412
CGX_RUNTIME_WARNINGS = not getattr(sys, "frozen", False)
13+
1514
DEBUG = bool(environ.get("CGX_DEBUG", False))
1615

1716
SUFFIX = "cgx"
@@ -29,64 +28,6 @@
2928
MOUSTACHES = re.compile(r"\{\{.*?\}\}")
3029

3130

32-
def load(path):
33-
"""
34-
Loads and returns a component from a CGX file.
35-
36-
A subclass of Component will be created from the CGX file
37-
where the contents of the <template> tag will be used as
38-
the `render` function, while the contents of the <script>
39-
tag will be used to provide the rest of the functions of
40-
the component.
41-
42-
For example:
43-
44-
<template>
45-
<item foo="bar">
46-
<item baz="bla"/>
47-
</item>
48-
</template
49-
50-
<script>
51-
import collagraph as cg
52-
53-
class Foo(cg.Component):
54-
pass
55-
</script>
56-
57-
"""
58-
template = path.read_text()
59-
60-
return load_from_string(template, path)
61-
62-
63-
def load_from_string(template, path=None):
64-
"""
65-
Load template from a string
66-
"""
67-
if path is None:
68-
path = "<template>"
69-
70-
# Construct the AST tree
71-
tree, name = construct_ast(path=path, template=template)
72-
73-
# Compile the tree into a code object (module)
74-
code = compile(tree, filename=str(path), mode="exec")
75-
# Execute the code as module and pass a dictionary that will capture
76-
# the global and local scope of the module
77-
module_namespace = {}
78-
exec(code, module_namespace)
79-
80-
# Check that the class definition is an actual subclass of Component
81-
component_class = module_namespace[name]
82-
if not issubclass(component_class, Component):
83-
raise ValueError(
84-
f"The last class defined in {path} is not a subclass of "
85-
f"Component: {component_class}"
86-
)
87-
return component_class, module_namespace
88-
89-
9031
def construct_ast(path, template=None):
9132
"""
9233
Returns a tuple of the constructed AST tree and name of (enhanced) component class.

collagraph/sfc/importer.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from importlib.abc import Loader, MetaPathFinder
5+
from importlib.machinery import ModuleSpec
6+
from pathlib import Path
7+
from types import ModuleType
8+
from typing import Sequence
9+
10+
from . import compiler, load
11+
12+
13+
class CGXLoader(Loader):
14+
"""Loader for .cgx files"""
15+
16+
def __init__(self, sfc_path):
17+
"""Create loader and store the referenced file"""
18+
self.sfc_path = sfc_path
19+
20+
def create_module(self, spec):
21+
# Return None to make use of the standard machinery for creating modules
22+
return None
23+
24+
def exec_module(self, module):
25+
"""Exec the compiled code, using the given module's __dict__ as namespace
26+
in order to instantiate the module"""
27+
load(self.sfc_path, namespace=module.__dict__)
28+
29+
30+
class CGXPathFinder(MetaPathFinder):
31+
"""MetaPathFinder for CGX files"""
32+
33+
def find_spec(
34+
self, name: str, path: Sequence[str] | None, target: ModuleType | None = None
35+
) -> ModuleSpec | None:
36+
# """Look for a cgx file based on the given name and return a ModuleSpec"""
37+
if target is not None:
38+
# Target is set when module is being reloaded.
39+
# In our case we can just return the existing spec.
40+
return target.__spec__
41+
42+
_package, _, module_name = name.rpartition(".")
43+
sfc_file_name = f"{module_name}.{compiler.SUFFIX}"
44+
directories = sys.path if path is None else path
45+
for directory in directories:
46+
sfc_path = Path(directory) / sfc_file_name
47+
if sfc_path.exists():
48+
spec = ModuleSpec(name, CGXLoader(sfc_path), origin=str(sfc_path))
49+
spec.has_location = True
50+
return spec
51+
52+
53+
# Add cgx path finder at the end of the list of finders
54+
sys.meta_path.append(CGXPathFinder())

collagraph/sfc/parser.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
from html.parser import HTMLParser
4+
from weakref import ref
5+
6+
7+
class TextElement:
8+
def __init__(self, content, location=None):
9+
self.content = content
10+
self.location = location
11+
12+
13+
class Comment:
14+
def __init__(self, content, location=None):
15+
self.content = content
16+
self.location = location
17+
18+
19+
class Element:
20+
"""Node that represents an element from a CGX file."""
21+
22+
def __init__(self, tag: str, attrs: dict, location: tuple[int, int]):
23+
self.tag = tag
24+
self.attrs = attrs or {}
25+
self.location = location
26+
self.end: tuple[int, int] | None = None
27+
self.data: str | None = None
28+
self.children: list[Element | Comment | TextElement] = []
29+
self.parent: ref | None = None
30+
31+
def child_with_tag(self, tag):
32+
for child in self.children:
33+
if getattr(child, "tag", None) == tag:
34+
return child
35+
36+
37+
class CGXParser(HTMLParser):
38+
"""Parser for CGX files.
39+
40+
Creates a tree of Nodes with all encountered attributes and data.
41+
"""
42+
43+
def __init__(self, *args, **kwargs):
44+
super().__init__(*args, **kwargs)
45+
self.root = Element("root", attrs={}, location=(-1, -1))
46+
self.stack = [self.root]
47+
48+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
49+
# The tag parameter is lower-cased by the HTMLParser.
50+
# In order to figure out whether the tag indicates
51+
# an imported class, we need the original casing for
52+
# the tag.
53+
# Using the original start tag, we can figure out where
54+
# the tag is located using a lower-cased version. And then
55+
# use the index to extract the original casing for the tag.
56+
complete_tag = self.get_starttag_text()
57+
index = complete_tag.lower().index(tag)
58+
original_tag = complete_tag[index : index + len(tag)]
59+
node = Element(original_tag, attrs=dict(attrs), location=self.getpos())
60+
61+
# Cast attributes that have no value to boolean (True)
62+
# so that they function like flags
63+
for key, value in node.attrs.items():
64+
if value is None:
65+
node.attrs[key] = True
66+
67+
# Add item as child to the last on the stack
68+
parent = self.stack[-1]
69+
parent.children.append(node)
70+
node.parent = ref(parent)
71+
# Make the new node the last on the stack
72+
self.stack.append(node)
73+
74+
def handle_endtag(self, tag: str):
75+
# pop it till popping the same tag in order to
76+
# work around unclosed tags?
77+
# Pop the stack until (but not the root!)
78+
while len(self.stack) > 1:
79+
node = self.stack.pop()
80+
node.end = self.getpos()
81+
if node.tag.lower() == tag:
82+
break
83+
84+
def handle_data(self, data: str):
85+
if data.strip():
86+
# Add item as child to the last on the stack
87+
self.stack[-1].children.append(
88+
TextElement(content=data, location=self.getpos())
89+
)
90+
91+
def handle_comment(self, data: str):
92+
if data.strip():
93+
# Add item as child to the last on the stack
94+
self.stack[-1].children.append(
95+
Comment(content=data, location=self.getpos())
96+
)

0 commit comments

Comments
 (0)