-
Notifications
You must be signed in to change notification settings - Fork 1
Push v0.3.3 to main #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,4 +13,4 @@ class Col(Symbol): | |
md = "" | ||
html = "col" | ||
rst = "" | ||
self_closing = True | ||
type = "void" |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if t.TYPE_CHECKING: | ||||||||||||||||||||||||||||||||||
from .symbol import Symbol | ||||||||||||||||||||||||||||||||||
from ..typing import ATTR_TYPES | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
T1 = t.TypeVar("T1") | ||||||||||||||||||||||||||||||||||
T2 = t.TypeVar("T2") | ||||||||||||||||||||||||||||||||||
|
@@ -10,6 +11,40 @@ | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
ARGS = t.ParamSpec("ARGS") | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
class HashableList(t.Generic[T1]): | ||||||||||||||||||||||||||||||||||
def __init__(self, lst:'list[T1]'): | ||||||||||||||||||||||||||||||||||
self.lst = lst | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __hash__(self): | ||||||||||||||||||||||||||||||||||
# Convert list to tuple for hashing | ||||||||||||||||||||||||||||||||||
return hash(tuple(self.lst)) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __eq__(self, other): | ||||||||||||||||||||||||||||||||||
if not isinstance(other, HashableList): | ||||||||||||||||||||||||||||||||||
return False | ||||||||||||||||||||||||||||||||||
return self.lst == other.lst | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __repr__(self): | ||||||||||||||||||||||||||||||||||
return f"HashableList({self.lst})" | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
class HashableDict(t.Generic[T1, T2]): | ||||||||||||||||||||||||||||||||||
def __init__(self, dct:'dict[T1, T2]'): | ||||||||||||||||||||||||||||||||||
self.dct = dct | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __hash__(self): | ||||||||||||||||||||||||||||||||||
# Convert list to tuple for hashing | ||||||||||||||||||||||||||||||||||
return hash(tuple(self.dct.items())) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __eq__(self, other): | ||||||||||||||||||||||||||||||||||
if not isinstance(other, HashableDict): | ||||||||||||||||||||||||||||||||||
return False | ||||||||||||||||||||||||||||||||||
return self.dct == other.dct | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __repr__(self): | ||||||||||||||||||||||||||||||||||
return f"HashableList({self.dct})" | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
class GetProtocol(t.Protocol, t.Generic[T1, T2]): | ||||||||||||||||||||||||||||||||||
def get(self, key: 'T1', ) -> 'T2': ... | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
|
@@ -20,18 +55,21 @@ def copy(self) -> 'T1': ... | |||||||||||||||||||||||||||||||||
class Copy: | ||||||||||||||||||||||||||||||||||
def __init__(self, data): | ||||||||||||||||||||||||||||||||||
self.data = data | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def copy(self): | ||||||||||||||||||||||||||||||||||
return self.data | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
T5 = t.TypeVar("T5", bound=CopyProtocol) | ||||||||||||||||||||||||||||||||||
HASHABLE_ATTRS = str | bool | int | float | HashableList['HASHABLE_ATTRS'] | HashableDict[str, 'HASHABLE_ATTRS'] | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
class Fetcher(t.Generic[T1, T2, T5]): | ||||||||||||||||||||||||||||||||||
def __init__(self, data: 'GetProtocol[T1, T2]', default:'T5'=Copy(None)): | ||||||||||||||||||||||||||||||||||
def __init__(self, data: 't.Union[GetProtocol[T1, T2], dict[T1, T2]]', default:'T5'=Copy(None)): | ||||||||||||||||||||||||||||||||||
self.data = data | ||||||||||||||||||||||||||||||||||
self.default = default.copy() if isinstance(default, CopyProtocol) else default | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def __getitem__(self, name:'T1') -> 'T2|T5': | ||||||||||||||||||||||||||||||||||
if isinstance(self.data, dict): | ||||||||||||||||||||||||||||||||||
return self.data.get(name, self.default) | ||||||||||||||||||||||||||||||||||
return self.data.get(name, self.default) | ||||||||||||||||||||||||||||||||||
class InnerHTML: | ||||||||||||||||||||||||||||||||||
def __init__(self, inner): | ||||||||||||||||||||||||||||||||||
|
@@ -40,35 +78,54 @@ def __init__(self, inner): | |||||||||||||||||||||||||||||||||
self.ids: 'dict[str|None, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.classes: 'dict[str, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.tags: 'dict[type[Symbol], list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.attrs: 'dict[str, dict[HASHABLE_ATTRS, list[Symbol]]]' = {} | ||||||||||||||||||||||||||||||||||
self.text: 'dict[str, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
self.children_ids: 'dict[str|None, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.children_classes: 'dict[str, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.children_tags: 'dict[type[Symbol], list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
self.children_attrs: 'dict[str, dict[str, list[Symbol]]]' = {} | ||||||||||||||||||||||||||||||||||
self.children_text: 'dict[str, list[Symbol]]' = {} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def add_elm(self, elm:'Symbol'): | ||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||
Add an element to the children indexes and merge the element's own indexes | ||||||||||||||||||||||||||||||||||
recursively into aggregate indexes. | ||||||||||||||||||||||||||||||||||
def add_elm(self, elm: 'Symbol'): | ||||||||||||||||||||||||||||||||||
def make_hashable(v): | ||||||||||||||||||||||||||||||||||
if isinstance(v, list): | ||||||||||||||||||||||||||||||||||
return HashableList(v) | ||||||||||||||||||||||||||||||||||
elif isinstance(v, dict): | ||||||||||||||||||||||||||||||||||
return HashableDict(v) | ||||||||||||||||||||||||||||||||||
return v | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||||||||||||
elm: Symbol element to add to the indexes. | ||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||
self.children_ids.setdefault(elm.get_prop("id", None), []).append(elm) | ||||||||||||||||||||||||||||||||||
[self.children_classes.setdefault(c, []).append(elm) for c in elm.classes] | ||||||||||||||||||||||||||||||||||
self.children_tags.setdefault(type(elm), []).append(elm) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def concat(d1: 'dict[T1|T3, list[T2|T4]]', *d2: 'dict[T3, list[T4]]', **kwargs): | ||||||||||||||||||||||||||||||||||
ret = {**kwargs} | ||||||||||||||||||||||||||||||||||
# Normalize keys when adding to children_attrs | ||||||||||||||||||||||||||||||||||
for prop, value in elm.props.items(): | ||||||||||||||||||||||||||||||||||
key = make_hashable(value) | ||||||||||||||||||||||||||||||||||
self.children_attrs.setdefault(prop, {}).setdefault(key, []).append(elm) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
for dict in list(d2) + [d1]: | ||||||||||||||||||||||||||||||||||
for k, v in dict.items(): | ||||||||||||||||||||||||||||||||||
self.children_text.setdefault(elm.text, []).append(elm) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def concat(d1: 'dict', *d2: 'dict'): | ||||||||||||||||||||||||||||||||||
ret = {} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
for dict_ in list(d2) + [d1]: | ||||||||||||||||||||||||||||||||||
for k, v in dict_.items(): | ||||||||||||||||||||||||||||||||||
ret.setdefault(k, []).extend(v) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ret | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
# Normalize keys in elm.props for attrs merging | ||||||||||||||||||||||||||||||||||
normalized_props = { | ||||||||||||||||||||||||||||||||||
prop: {make_hashable(value): [elm] for value in values} | ||||||||||||||||||||||||||||||||||
for prop, values in elm.props.items() | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
Comment on lines
+118
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a prop value is a scalar ( Consider: -normalized_props = {
- prop: {make_hashable(value): [elm] for value in values}
- for prop, values in elm.props.items()
-}
+normalized_props: dict[str, dict[HASHABLE_ATTRS, list[Symbol]]] = {}
+for prop, val in elm.props.items():
+ key = make_hashable(val)
+ normalized_props.setdefault(prop, {})[key] = [elm] 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
self.ids = concat(self.ids, elm.inner_html.ids, {elm.get_prop("id", None): [elm]}) | ||||||||||||||||||||||||||||||||||
self.classes = concat(self.classes, elm.inner_html.classes, {c: [elm] for c in elm.classes}) | ||||||||||||||||||||||||||||||||||
self.tags = concat(self.tags, elm.inner_html.tags, {type(elm): [elm]}) | ||||||||||||||||||||||||||||||||||
self.attrs = concat(self.attrs, elm.inner_html.attrs, normalized_props) | ||||||||||||||||||||||||||||||||||
self.text = concat(self.text, elm.inner_html.text, {elm.text: [elm]}) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def get_elements_by_id(self, id: 'str'): | ||||||||||||||||||||||||||||||||||
return self.ids.get(id, []) | ||||||||||||||||||||||||||||||||||
|
@@ -77,7 +134,45 @@ def get_elements_by_class_name(self, class_name: 'str'): | |||||||||||||||||||||||||||||||||
return self.classes.get(class_name, []) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def get_elements_by_tag_name(self, tag: 'str'): | ||||||||||||||||||||||||||||||||||
return self.tags.get(tag, []) | ||||||||||||||||||||||||||||||||||
# Find the tag class by name | ||||||||||||||||||||||||||||||||||
for tag_class, elements in self.tags.items(): | ||||||||||||||||||||||||||||||||||
if tag_class.__name__.lower() == tag.lower(): | ||||||||||||||||||||||||||||||||||
return elements | ||||||||||||||||||||||||||||||||||
return [] | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def find(self, key:'str'): | ||||||||||||||||||||||||||||||||||
if key.startswith("#"): | ||||||||||||||||||||||||||||||||||
return self.get_elements_by_id(key[1:]) | ||||||||||||||||||||||||||||||||||
elif key.startswith("."): | ||||||||||||||||||||||||||||||||||
return self.get_elements_by_class_name(key[1:]) | ||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||
return self.get_elements_by_tag_name(key) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def get_by_text(self, text:'str'): | ||||||||||||||||||||||||||||||||||
return self.text.get(text, []) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
def get_by_attr(self, attr:'str', value:'str'): | ||||||||||||||||||||||||||||||||||
return self.attrs.get(attr, {}).get(value, []) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
Comment on lines
+155
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
Look-ups with list/dict values will fail because keys are stored in their -def get_by_attr(self, attr: str, value: str):
- return self.attrs.get(attr, {}).get(value, [])
+def get_by_attr(self, attr: str, value):
+ key = value if isinstance(value, (str, int, float, bool)) else make_hashable(value)
+ return self.attrs.get(attr, {}).get(key, []) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
def advanced_find(self, tag:'str', attrs:'dict[t.Literal["text"] | str, str | bool | int | float | tuple[str, str | bool | int | float] | list[str | bool | int | float | tuple[str, str | bool | int | float]]]' = {}): | ||||||||||||||||||||||||||||||||||
def check_attr(e:'Symbol', k:'str', v:'str | bool | int | float | tuple[str, str | bool | int | float]'): | ||||||||||||||||||||||||||||||||||
Comment on lines
+157
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Mutable default for Using -def advanced_find(self, tag: str,
- attrs: dict[...]= {}):
+def advanced_find(self, tag: str,
+ attrs: dict[...]|None = None):
...
- if "text" in attrs:
+ attrs = attrs or {}
+ if "text" in attrs: 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.8.2)157-157: Do not use mutable data structures for argument defaults Replace with (B006) |
||||||||||||||||||||||||||||||||||
prop = e.get_prop(k) | ||||||||||||||||||||||||||||||||||
if isinstance(prop, list): | ||||||||||||||||||||||||||||||||||
return v in prop | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if isinstance(prop, dict): | ||||||||||||||||||||||||||||||||||
return v in list(prop.items()) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return prop == v | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
tags = self.find(tag) | ||||||||||||||||||||||||||||||||||
if "text" in attrs: | ||||||||||||||||||||||||||||||||||
text = attrs.pop("text") | ||||||||||||||||||||||||||||||||||
tags = filter(lambda e: e.text == text, tags) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
for k, v in attrs.items(): | ||||||||||||||||||||||||||||||||||
tags = filter(lambda e: check_attr(e, k, v) if not isinstance(v, list) else all([check_attr(e, k, i) for i in v]), tags) | ||||||||||||||||||||||||||||||||||
return list(tags) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
@property | ||||||||||||||||||||||||||||||||||
def id(self): | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid calling
Copy()
in default argument (B008)Default arguments are evaluated at import time. Replace with
None
andinitialise inside the function to prevent accidental state sharing.
🧰 Tools
🪛 Ruff (0.8.2)
66-66: Do not perform function call
Copy
in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)