compone 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compone-0.1.0/PKG-INFO +25 -0
- compone-0.1.0/README.md +6 -0
- compone-0.1.0/compone/__init__.py +3 -0
- compone-0.1.0/compone/component.py +409 -0
- compone-0.1.0/compone/escape.py +26 -0
- compone-0.1.0/compone/html/__init__.py +95 -0
- compone-0.1.0/compone/html/content_sectioning.py +16 -0
- compone-0.1.0/compone/html/embedded.py +8 -0
- compone-0.1.0/compone/html/forms.py +31 -0
- compone-0.1.0/compone/html/inline_text.py +34 -0
- compone-0.1.0/compone/html/interactive.py +5 -0
- compone-0.1.0/compone/html/main.py +4 -0
- compone-0.1.0/compone/html/metadata.py +38 -0
- compone-0.1.0/compone/html/multimedia.py +8 -0
- compone-0.1.0/compone/html/other.py +4 -0
- compone-0.1.0/compone/html/scripting.py +5 -0
- compone-0.1.0/compone/html/table.py +12 -0
- compone-0.1.0/compone/html/text_content.py +36 -0
- compone-0.1.0/compone/html/web_components.py +4 -0
- compone-0.1.0/compone/robots/__init__.py +3 -0
- compone-0.1.0/compone/robots/html_meta.py +40 -0
- compone-0.1.0/compone/robots/robots_txt.py +73 -0
- compone-0.1.0/compone/utils.py +7 -0
- compone-0.1.0/compone/xml.py +11 -0
- compone-0.1.0/pyproject.toml +18 -0
compone-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: compone
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Component framework for Python
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: György Kiss
|
|
7
|
+
Author-email: gyorgy@duck.com
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Dist: markupsafe (>=2.1.5,<3.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Compone
|
|
20
|
+
|
|
21
|
+
Compone is a **Python component framework** inspired by React, that allows for
|
|
22
|
+
type-safe HTML generation. It is an alternative to Python template engines,
|
|
23
|
+
using Python objects instead of strings. This way you get Exception tracebacks
|
|
24
|
+
for mistyped HTML tags and attributes instead of malformed HTML!
|
|
25
|
+
|
compone-0.1.0/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Compone
|
|
2
|
+
|
|
3
|
+
Compone is a **Python component framework** inspired by React, that allows for
|
|
4
|
+
type-safe HTML generation. It is an alternative to Python template engines,
|
|
5
|
+
using Python objects instead of strings. This way you get Exception tracebacks
|
|
6
|
+
for mistyped HTML tags and attributes instead of malformed HTML!
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import inspect
|
|
3
|
+
import keyword
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Callable, Iterable, List, Optional, Protocol, Type, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
from .escape import escape, safe
|
|
10
|
+
from .utils import _is_iterable
|
|
11
|
+
|
|
12
|
+
# previous frame id (frame.f_back), parent object
|
|
13
|
+
last_parent = ContextVar("last_parent", default=(None, None))
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
CompSelf = TypeVar("CompSelf", bound="_ComponentBase")
|
|
17
|
+
ChildSelf = TypeVar("ChildSelf", bound="_ChildrenBase")
|
|
18
|
+
StrType = Union[str, safe]
|
|
19
|
+
ContentType = Union[StrType, CompSelf]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ComponentClass(Protocol):
|
|
23
|
+
def render(self, children: safe) -> Union[ContentType, Iterable[ContentType]]:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ComponentBase:
|
|
28
|
+
_sig: inspect.Signature
|
|
29
|
+
_positional_args: List[str]
|
|
30
|
+
_var_keyword: Optional[str]
|
|
31
|
+
|
|
32
|
+
def __init__(self, *args, **kwargs):
|
|
33
|
+
self._bound_args = self._bind_args(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def _bind_args(self, *args, **kwargs):
|
|
36
|
+
bound = self._sig.bind(*args, **kwargs)
|
|
37
|
+
bound.apply_defaults()
|
|
38
|
+
return bound
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def props(self) -> dict:
|
|
42
|
+
kwargs = {k: v for k, v in self._bound_args.kwargs.items() if v is not None}
|
|
43
|
+
args = {
|
|
44
|
+
k: v
|
|
45
|
+
for k, v in self._bound_args.arguments.items()
|
|
46
|
+
if k not in kwargs and v is not None
|
|
47
|
+
}
|
|
48
|
+
if self._var_keyword is not None:
|
|
49
|
+
del args[self._var_keyword]
|
|
50
|
+
return MappingProxyType({**args, **kwargs})
|
|
51
|
+
|
|
52
|
+
def replace(self, **kwargs) -> CompSelf:
|
|
53
|
+
self._check_common_props(kwargs)
|
|
54
|
+
return self._make_new(kwargs)
|
|
55
|
+
|
|
56
|
+
def append(self, **kwargs) -> CompSelf:
|
|
57
|
+
self._check_common_props(kwargs)
|
|
58
|
+
appended = {key: (self.props[key] + val) for key, val in kwargs.items()}
|
|
59
|
+
return self._make_new(appended)
|
|
60
|
+
|
|
61
|
+
def _check_common_props(self, kwargs):
|
|
62
|
+
if not set(kwargs) & set(self.props):
|
|
63
|
+
kwargs_list = ", ".join(repr(k) for k in kwargs.keys())
|
|
64
|
+
raise TypeError(f"{self!r} has no existing props for {kwargs_list}")
|
|
65
|
+
|
|
66
|
+
def __call__(self, *args, **kwargs) -> CompSelf:
|
|
67
|
+
# Replaces existing props, set new ones
|
|
68
|
+
args_only = {name: newval for name, newval in zip(self._positional_args, args)}
|
|
69
|
+
return self._make_new({**args_only, **kwargs})
|
|
70
|
+
|
|
71
|
+
def _make_new(self, new_arguments) -> CompSelf:
|
|
72
|
+
arguments_copy = {
|
|
73
|
+
k: copy.copy(v) for k, v in self._bound_args.arguments.items()
|
|
74
|
+
}
|
|
75
|
+
bound_copy = inspect.BoundArguments(self._sig, arguments_copy)
|
|
76
|
+
|
|
77
|
+
if self._var_keyword is None:
|
|
78
|
+
bound_copy.arguments.update(new_arguments)
|
|
79
|
+
else:
|
|
80
|
+
old_star_arguments = bound_copy.arguments[self._var_keyword]
|
|
81
|
+
new_star_arguments = {
|
|
82
|
+
k: v for k, v in new_arguments.items() if k in old_star_arguments
|
|
83
|
+
}
|
|
84
|
+
old_star_arguments.update(new_star_arguments)
|
|
85
|
+
|
|
86
|
+
other_arguments = {
|
|
87
|
+
k: v for k, v in new_arguments.items() if k not in new_star_arguments
|
|
88
|
+
}
|
|
89
|
+
bound_copy.arguments.update(other_arguments)
|
|
90
|
+
|
|
91
|
+
# This is to add new kwargs that was not specified before
|
|
92
|
+
# It will raise TypeError for args not in self._sig
|
|
93
|
+
extra_kwargs = {
|
|
94
|
+
k: v
|
|
95
|
+
for k, v in new_arguments.items()
|
|
96
|
+
if k not in bound_copy.kwargs and k not in self._positional_args
|
|
97
|
+
}
|
|
98
|
+
new_kwargs = {**bound_copy.kwargs, **extra_kwargs}
|
|
99
|
+
new_bound = self._bind_args(*bound_copy.args, **new_kwargs)
|
|
100
|
+
return self.__class__(*new_bound.args, **new_bound.kwargs)
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
if self.props:
|
|
104
|
+
proplist = ", ".join(f"{k}={v!r}" for k, v in self.props.items())
|
|
105
|
+
else:
|
|
106
|
+
proplist = ""
|
|
107
|
+
return f"<{self.__class__.__name__}({proplist})>"
|
|
108
|
+
|
|
109
|
+
def __mul__(self, other: int) -> CompSelf:
|
|
110
|
+
if not isinstance(other, int):
|
|
111
|
+
return NotImplemented
|
|
112
|
+
|
|
113
|
+
# Every component is safe by default, so the result should be safe too
|
|
114
|
+
Multiple = Component(lambda: safe(self) * other)
|
|
115
|
+
Multiple.__name__ = "Multi" + self.__class__.__name__
|
|
116
|
+
Multiple.__doc__ = "Multiple: " + (self.__doc__ or "")
|
|
117
|
+
return Multiple()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _ChildrenBase(_ComponentBase):
|
|
121
|
+
def __init__(self, *args, **kwargs):
|
|
122
|
+
super().__init__(*args, **kwargs)
|
|
123
|
+
self._children = []
|
|
124
|
+
|
|
125
|
+
def __enter__(self) -> ChildSelf:
|
|
126
|
+
parent_frame_id, parent = last_parent.get()
|
|
127
|
+
self._parent_frame_id = parent_frame_id
|
|
128
|
+
self._parent = parent
|
|
129
|
+
current_frame_id = id(inspect.currentframe().f_back)
|
|
130
|
+
last_parent.set((current_frame_id, self))
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
134
|
+
current_frame_id = id(inspect.currentframe().f_back)
|
|
135
|
+
if self._parent is not None and current_frame_id == self._parent_frame_id:
|
|
136
|
+
self._parent._children.append(self)
|
|
137
|
+
last_parent.set((self._parent_frame_id, self._parent))
|
|
138
|
+
|
|
139
|
+
def __iadd__(self, other) -> ChildSelf:
|
|
140
|
+
self._children.append(other)
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __class_getitem__(cls, key) -> ChildSelf:
|
|
144
|
+
return cls()[key]
|
|
145
|
+
|
|
146
|
+
def __getitem__(self, children) -> safe:
|
|
147
|
+
if self._children:
|
|
148
|
+
# Not AttributeError because it would be confusing,
|
|
149
|
+
# for example when using hasattr.
|
|
150
|
+
# Also this is like a function call, so ValueError makes sense
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"Component already has children, "
|
|
153
|
+
"use the += operator if you want to add more."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if _is_iterable(children):
|
|
157
|
+
# str is a special case, because it's an iterator too.
|
|
158
|
+
# _ChildrenBase are also iterators because of this very method
|
|
159
|
+
if isinstance(children, (str, safe, _ChildrenBase)):
|
|
160
|
+
children = (children,)
|
|
161
|
+
else:
|
|
162
|
+
children = (children,)
|
|
163
|
+
|
|
164
|
+
new = self.__class__(*self._bound_args.args, **self._bound_args.kwargs)
|
|
165
|
+
new._children = children
|
|
166
|
+
return new
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def children(self):
|
|
170
|
+
return tuple(self._children)
|
|
171
|
+
|
|
172
|
+
def __call__(self, *args, **kwargs) -> CompSelf:
|
|
173
|
+
if self._children:
|
|
174
|
+
raise ValueError("Component already has children, cannot replace them.")
|
|
175
|
+
return super().__call__(*args, **kwargs)
|
|
176
|
+
|
|
177
|
+
def __eq__(self, other):
|
|
178
|
+
if not isinstance(other, _ChildrenBase):
|
|
179
|
+
return NotImplemented
|
|
180
|
+
# This is the safest way to do this, because user-implemented,
|
|
181
|
+
# class-based Components can have different properties, which
|
|
182
|
+
# might render them differently. We can't compare those without rendering.
|
|
183
|
+
return str(self) == str(other)
|
|
184
|
+
|
|
185
|
+
def __str__(self) -> safe:
|
|
186
|
+
safe_children = self._escape(self._children) if self._children else safe()
|
|
187
|
+
content = self._render(safe_children)
|
|
188
|
+
return self._escape(content)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def _escape(cls, item) -> safe:
|
|
192
|
+
if isinstance(item, (str, _ComponentBase)):
|
|
193
|
+
return escape(item)
|
|
194
|
+
elif _is_iterable(item):
|
|
195
|
+
return safe("".join(cls._escape(e) for e in item))
|
|
196
|
+
else:
|
|
197
|
+
return escape(item)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _FuncComponent(_ChildrenBase):
|
|
201
|
+
_func: Callable
|
|
202
|
+
_pass_children: bool
|
|
203
|
+
|
|
204
|
+
def _render(self, children: safe) -> Union[ContentType, Iterable[ContentType]]:
|
|
205
|
+
# BoundArguments.kwargs is a property, this makes a copy
|
|
206
|
+
kwargs = self._bound_args.kwargs
|
|
207
|
+
|
|
208
|
+
if self._pass_children:
|
|
209
|
+
kwargs["children"] = children
|
|
210
|
+
|
|
211
|
+
# self.func is unbound
|
|
212
|
+
content = self.__class__._func(*self._bound_args.args, **kwargs)
|
|
213
|
+
return content
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class _ClassComponent(_ChildrenBase):
|
|
217
|
+
_pass_children: bool
|
|
218
|
+
_user_class: ComponentClass
|
|
219
|
+
|
|
220
|
+
@cached_property
|
|
221
|
+
def _user_instance(self) -> ComponentClass:
|
|
222
|
+
return self._user_class(*self._bound_args.args, **self._bound_args.kwargs)
|
|
223
|
+
|
|
224
|
+
def _render(self, children: safe):
|
|
225
|
+
if self._pass_children:
|
|
226
|
+
return self._user_instance.render(children)
|
|
227
|
+
else:
|
|
228
|
+
return self._user_instance.render()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class _HTMLComponentBase(_ComponentBase):
|
|
232
|
+
_html_tag: str
|
|
233
|
+
_attributes = None
|
|
234
|
+
_sig = inspect.signature(lambda **kwargs: None)
|
|
235
|
+
_var_keyword = "kwargs"
|
|
236
|
+
_positional_args = []
|
|
237
|
+
|
|
238
|
+
def __init__(self, **kwargs):
|
|
239
|
+
if self._attributes is not None:
|
|
240
|
+
kwargs.update(self._attributes)
|
|
241
|
+
self._original_kwargs = kwargs
|
|
242
|
+
self._convert_class(kwargs)
|
|
243
|
+
super().__init__(**kwargs)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _convert_class(kwargs) -> list:
|
|
247
|
+
class_ = kwargs.get("class_", None)
|
|
248
|
+
if isinstance(class_, str):
|
|
249
|
+
kwargs["class_"] = class_.split()
|
|
250
|
+
elif isinstance(class_, (list, tuple)):
|
|
251
|
+
kwargs["class_"] = [c.strip() for c in class_]
|
|
252
|
+
|
|
253
|
+
def append(self, **kwargs) -> CompSelf:
|
|
254
|
+
self._convert_class(kwargs)
|
|
255
|
+
return super().append(**kwargs)
|
|
256
|
+
|
|
257
|
+
def _get_attributes(self) -> str: # noqa: C901
|
|
258
|
+
bool_args = []
|
|
259
|
+
keyval_args = []
|
|
260
|
+
|
|
261
|
+
for key, val in self.props.items():
|
|
262
|
+
if isinstance(val, str) and '"' in val and "'" in val:
|
|
263
|
+
raise ValueError("Both single and double quotes in attribute value")
|
|
264
|
+
if keyword.iskeyword(no_underscore := key[:-1]):
|
|
265
|
+
key = no_underscore
|
|
266
|
+
if isinstance(val, (list, tuple)):
|
|
267
|
+
val = " ".join(str(i) for i in val)
|
|
268
|
+
|
|
269
|
+
html_key = escape(key.replace("_", "-"))
|
|
270
|
+
|
|
271
|
+
if isinstance(val, bool):
|
|
272
|
+
# by HTML standard, False values must not be included in attributes
|
|
273
|
+
if not val:
|
|
274
|
+
continue
|
|
275
|
+
bool_args.append(html_key)
|
|
276
|
+
else:
|
|
277
|
+
html_val = escape(val)
|
|
278
|
+
if '"' in html_val:
|
|
279
|
+
html_attr = f"{html_key}='{html_val}'"
|
|
280
|
+
else:
|
|
281
|
+
html_attr = f'{html_key}="{html_val}"'
|
|
282
|
+
keyval_args.append(html_attr)
|
|
283
|
+
|
|
284
|
+
bool_prefix = " " if bool_args else ""
|
|
285
|
+
bool_arguments = " ".join(bool_args)
|
|
286
|
+
|
|
287
|
+
keyval_prefix = " " if keyval_args else ""
|
|
288
|
+
keyval_arguments = " ".join(keyval_args)
|
|
289
|
+
|
|
290
|
+
return bool_prefix + bool_arguments + keyval_prefix + keyval_arguments
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _HTMLComponent(_HTMLComponentBase, _ChildrenBase):
|
|
294
|
+
def _render(self, children: safe) -> safe:
|
|
295
|
+
attributes = self._get_attributes()
|
|
296
|
+
return safe(f"<{self._html_tag}{attributes}>{children}</{self._html_tag}>")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class _SelfClosingHTMLComponent(_HTMLComponentBase):
|
|
300
|
+
def __str__(self) -> safe:
|
|
301
|
+
attributes = self._get_attributes()
|
|
302
|
+
return safe(f"<{self._html_tag}{attributes} />")
|
|
303
|
+
|
|
304
|
+
def __eq__(self, other):
|
|
305
|
+
if not isinstance(other, _SelfClosingHTMLComponent):
|
|
306
|
+
return NotImplemented
|
|
307
|
+
return (
|
|
308
|
+
# It's a little bit cheaper to compare this way than rendering
|
|
309
|
+
self._html_tag == other._html_tag
|
|
310
|
+
and self._original_kwargs == other._original_kwargs
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def Component(
|
|
315
|
+
func_or_class: Union[ComponentClass, Callable],
|
|
316
|
+
) -> Union[Type[_ClassComponent], Type[_FuncComponent]]:
|
|
317
|
+
if inspect.isfunction(func_or_class):
|
|
318
|
+
return _make_func_component(func_or_class)
|
|
319
|
+
elif inspect.isclass(func_or_class):
|
|
320
|
+
return _make_class_component(func_or_class)
|
|
321
|
+
else:
|
|
322
|
+
raise TypeError("Components can only be classes or functions")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _make_sig(func):
|
|
326
|
+
sig = inspect.signature(func)
|
|
327
|
+
# This is only for caching in the class
|
|
328
|
+
positional_args = [
|
|
329
|
+
name
|
|
330
|
+
for name, param in sig.parameters.items()
|
|
331
|
+
if param.kind in {param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD}
|
|
332
|
+
]
|
|
333
|
+
return sig, positional_args
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _get_var_keyword(sig) -> Optional[str]:
|
|
337
|
+
try:
|
|
338
|
+
return next(p.name for p in sig.parameters.values() if p.kind == p.VAR_KEYWORD)
|
|
339
|
+
except StopIteration:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _make_class_component(user_class: ComponentClass) -> Type[_ClassComponent]:
|
|
344
|
+
if not hasattr(user_class, "render"):
|
|
345
|
+
raise TypeError(f"{user_class.__name__} doesn't have a .render() method.")
|
|
346
|
+
|
|
347
|
+
orig_sig, positional_args = _make_sig(user_class.__init__)
|
|
348
|
+
without_passed = [
|
|
349
|
+
param
|
|
350
|
+
for key, param in orig_sig.parameters.items()
|
|
351
|
+
if key not in {"self", "children"}
|
|
352
|
+
]
|
|
353
|
+
sig = inspect.Signature(parameters=without_passed)
|
|
354
|
+
render_sig = inspect.signature(user_class.render)
|
|
355
|
+
|
|
356
|
+
return type(
|
|
357
|
+
user_class.__name__,
|
|
358
|
+
(_ClassComponent,),
|
|
359
|
+
dict(
|
|
360
|
+
_user_class=user_class,
|
|
361
|
+
_sig=sig,
|
|
362
|
+
_positional_args=positional_args,
|
|
363
|
+
_var_keyword=_get_var_keyword(sig),
|
|
364
|
+
_pass_children="children" in render_sig.parameters,
|
|
365
|
+
__module__=user_class.__module__,
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _make_func_component(func: Callable) -> Type[_FuncComponent]:
|
|
371
|
+
orig_sig, positional_args = _make_sig(func)
|
|
372
|
+
without_children = [
|
|
373
|
+
param for key, param in orig_sig.parameters.items() if key != "children"
|
|
374
|
+
]
|
|
375
|
+
sig = inspect.Signature(parameters=without_children)
|
|
376
|
+
|
|
377
|
+
return type(
|
|
378
|
+
func.__name__,
|
|
379
|
+
(_FuncComponent,),
|
|
380
|
+
dict(
|
|
381
|
+
_func=func,
|
|
382
|
+
_sig=sig,
|
|
383
|
+
_positional_args=positional_args,
|
|
384
|
+
_var_keyword=_get_var_keyword(orig_sig),
|
|
385
|
+
_pass_children="children" in orig_sig.parameters,
|
|
386
|
+
__module__=func.__module__,
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _HtmlElem(html_tag: str, parent_class: T) -> T:
|
|
392
|
+
return type(
|
|
393
|
+
html_tag.capitalize(),
|
|
394
|
+
(parent_class,),
|
|
395
|
+
dict(
|
|
396
|
+
_html_tag=html_tag,
|
|
397
|
+
__module__="compone.html",
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _Elem(html_tag: str) -> Type[_HTMLComponent]:
|
|
403
|
+
"""Create Component from HTML element on the fly."""
|
|
404
|
+
return _HtmlElem(html_tag, _HTMLComponent)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _SelfElem(html_tag: str) -> Type[_SelfClosingHTMLComponent]:
|
|
408
|
+
"""Create Component from self-closing HTML element on the fly."""
|
|
409
|
+
return _HtmlElem(html_tag, _SelfClosingHTMLComponent)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from markupsafe import Markup
|
|
4
|
+
from markupsafe import escape as markupsafe_escape
|
|
5
|
+
|
|
6
|
+
__all__ = ["escape", "safe"]
|
|
7
|
+
|
|
8
|
+
# Alias, because this is more generic than HTML and make sure
|
|
9
|
+
# that the API is future proof in case we change implementation
|
|
10
|
+
safe = Markup
|
|
11
|
+
"""Exclude a string from autoescaping using Markupsafe."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def escape(s: Any) -> safe:
|
|
15
|
+
"""Replace special characters to HTML/XML-safe sequences.
|
|
16
|
+
Marks the resulting string as safe with Markupsafe.
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(s, safe):
|
|
19
|
+
return s
|
|
20
|
+
elif s is None:
|
|
21
|
+
return safe()
|
|
22
|
+
# We use the __str__ method instead of __html__
|
|
23
|
+
elif hasattr(s, "__str__"):
|
|
24
|
+
s = s.__str__()
|
|
25
|
+
|
|
26
|
+
return markupsafe_escape(s)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
from ..component import _Elem
|
|
3
|
+
from .content_sectioning import (
|
|
4
|
+
H1,
|
|
5
|
+
H2,
|
|
6
|
+
H3,
|
|
7
|
+
H4,
|
|
8
|
+
H5,
|
|
9
|
+
H6,
|
|
10
|
+
Address,
|
|
11
|
+
Article,
|
|
12
|
+
Aside,
|
|
13
|
+
Footer,
|
|
14
|
+
Header,
|
|
15
|
+
Main,
|
|
16
|
+
Nav,
|
|
17
|
+
Section,
|
|
18
|
+
)
|
|
19
|
+
from .embedded import Embed, Iframe, Object, Picture, Portal, Source
|
|
20
|
+
from .forms import (
|
|
21
|
+
Button,
|
|
22
|
+
ButtonButton,
|
|
23
|
+
Datalist,
|
|
24
|
+
Fieldset,
|
|
25
|
+
Form,
|
|
26
|
+
Input,
|
|
27
|
+
Label,
|
|
28
|
+
Legend,
|
|
29
|
+
Meter,
|
|
30
|
+
Optgroup,
|
|
31
|
+
Option,
|
|
32
|
+
Output,
|
|
33
|
+
Progress,
|
|
34
|
+
ResetButton,
|
|
35
|
+
Select,
|
|
36
|
+
SubmitButton,
|
|
37
|
+
Textarea,
|
|
38
|
+
)
|
|
39
|
+
from .inline_text import (
|
|
40
|
+
A,
|
|
41
|
+
Abbr,
|
|
42
|
+
B,
|
|
43
|
+
Bdi,
|
|
44
|
+
Bdo,
|
|
45
|
+
Br,
|
|
46
|
+
Cite,
|
|
47
|
+
Code,
|
|
48
|
+
Data,
|
|
49
|
+
Del,
|
|
50
|
+
Dfn,
|
|
51
|
+
Em,
|
|
52
|
+
I,
|
|
53
|
+
Ins,
|
|
54
|
+
Kbd,
|
|
55
|
+
Mark,
|
|
56
|
+
Q,
|
|
57
|
+
Rp,
|
|
58
|
+
Rt,
|
|
59
|
+
Ruby,
|
|
60
|
+
S,
|
|
61
|
+
Samp,
|
|
62
|
+
Small,
|
|
63
|
+
Span,
|
|
64
|
+
Strong,
|
|
65
|
+
Sub,
|
|
66
|
+
Sup,
|
|
67
|
+
Time,
|
|
68
|
+
U,
|
|
69
|
+
Var,
|
|
70
|
+
Wbr,
|
|
71
|
+
)
|
|
72
|
+
from .interactive import Details, Dialog, Summary
|
|
73
|
+
from .main import Body, Html
|
|
74
|
+
from .metadata import Base, Head, Link, Meta, Style, Title
|
|
75
|
+
from .multimedia import Area, Audio, Img, Map, Track, Video
|
|
76
|
+
from .other import Math, Svg
|
|
77
|
+
from .scripting import Canvas, Noscript, Script
|
|
78
|
+
from .table import Caption, Col, Colgroup, Table, Tbody, Td, Tfoot, Th, Thead, Tr
|
|
79
|
+
from .text_content import (
|
|
80
|
+
Blockquote,
|
|
81
|
+
Dd,
|
|
82
|
+
Div,
|
|
83
|
+
Dl,
|
|
84
|
+
Dt,
|
|
85
|
+
Figcaption,
|
|
86
|
+
Figure,
|
|
87
|
+
Hr,
|
|
88
|
+
Li,
|
|
89
|
+
Menu,
|
|
90
|
+
Ol,
|
|
91
|
+
P,
|
|
92
|
+
Pre,
|
|
93
|
+
Ul,
|
|
94
|
+
)
|
|
95
|
+
from .web_components import Slot, Template
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ..component import _Elem
|
|
2
|
+
|
|
3
|
+
Address = _Elem("address")
|
|
4
|
+
Article = _Elem("article")
|
|
5
|
+
Aside = _Elem("aside")
|
|
6
|
+
Footer = _Elem("footer")
|
|
7
|
+
Header = _Elem("header")
|
|
8
|
+
H1 = _Elem("h1")
|
|
9
|
+
H2 = _Elem("h2")
|
|
10
|
+
H3 = _Elem("h3")
|
|
11
|
+
H4 = _Elem("h4")
|
|
12
|
+
H5 = _Elem("h5")
|
|
13
|
+
H6 = _Elem("h6")
|
|
14
|
+
Main = _Elem("main")
|
|
15
|
+
Nav = _Elem("nav")
|
|
16
|
+
Section = _Elem("section")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ..component import _Elem, _HTMLComponent, _SelfElem
|
|
2
|
+
|
|
3
|
+
Datalist = _Elem("datalist")
|
|
4
|
+
Fieldset = _Elem("fieldset")
|
|
5
|
+
Button = _Elem("button")
|
|
6
|
+
Form = _Elem("form")
|
|
7
|
+
Input = _SelfElem("input")
|
|
8
|
+
Label = _Elem("label")
|
|
9
|
+
Legend = _Elem("legend")
|
|
10
|
+
Meter = _Elem("meter")
|
|
11
|
+
Optgroup = _Elem("optgroup")
|
|
12
|
+
Option = _Elem("option")
|
|
13
|
+
Output = _Elem("output")
|
|
14
|
+
Progress = _Elem("progress")
|
|
15
|
+
Select = _Elem("select")
|
|
16
|
+
Textarea = _Elem("textarea")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ButtonButton(_HTMLComponent):
|
|
20
|
+
_html_tag = "button"
|
|
21
|
+
_attributes = {"type": "button"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResetButton(_HTMLComponent):
|
|
25
|
+
_html_tag = "button"
|
|
26
|
+
_attributes = {"type": "reset"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SubmitButton(_HTMLComponent):
|
|
30
|
+
_html_tag = "button"
|
|
31
|
+
_attributes = {"type": "submit"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from ..component import _Elem
|
|
2
|
+
from ..escape import safe
|
|
3
|
+
|
|
4
|
+
A = _Elem("a")
|
|
5
|
+
Abbr = _Elem("abbr")
|
|
6
|
+
B = _Elem("b")
|
|
7
|
+
Bdi = _Elem("bdi")
|
|
8
|
+
Bdo = _Elem("bdo")
|
|
9
|
+
Br = safe("<br>")
|
|
10
|
+
Cite = _Elem("cite")
|
|
11
|
+
Code = _Elem("code")
|
|
12
|
+
Data = _Elem("data")
|
|
13
|
+
Del = _Elem("del")
|
|
14
|
+
Dfn = _Elem("dfn")
|
|
15
|
+
Em = _Elem("em")
|
|
16
|
+
I = _Elem("i") # noqa: E741
|
|
17
|
+
Ins = _Elem("ins")
|
|
18
|
+
Kbd = _Elem("kbd")
|
|
19
|
+
Mark = _Elem("mark")
|
|
20
|
+
Q = _Elem("q")
|
|
21
|
+
Rp = _Elem("rp")
|
|
22
|
+
Rt = _Elem("rt")
|
|
23
|
+
Ruby = _Elem("ruby")
|
|
24
|
+
S = _Elem("s")
|
|
25
|
+
Samp = _Elem("samp")
|
|
26
|
+
Small = _Elem("small")
|
|
27
|
+
Span = _Elem("span")
|
|
28
|
+
Strong = _Elem("strong")
|
|
29
|
+
Sub = _Elem("sub")
|
|
30
|
+
Sup = _Elem("sup")
|
|
31
|
+
Time = _Elem("time")
|
|
32
|
+
U = _Elem("u")
|
|
33
|
+
Var = _Elem("var")
|
|
34
|
+
Wbr = safe("<wbr />")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from ..component import Component, _Elem, _SelfElem
|
|
4
|
+
from ..escape import safe
|
|
5
|
+
|
|
6
|
+
Base = _Elem("base")
|
|
7
|
+
Head = _Elem("head")
|
|
8
|
+
Link = _SelfElem("link")
|
|
9
|
+
Style = _Elem("style")
|
|
10
|
+
Title = _Elem("title")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@Component
|
|
14
|
+
def Meta(
|
|
15
|
+
*,
|
|
16
|
+
name: str,
|
|
17
|
+
http_equiv: Optional[str] = None,
|
|
18
|
+
itemprop: Optional[str] = None,
|
|
19
|
+
content: Optional[str] = None,
|
|
20
|
+
):
|
|
21
|
+
kwargs = {"name": name}
|
|
22
|
+
if http_equiv:
|
|
23
|
+
kwargs["http-equiv"] = http_equiv
|
|
24
|
+
if content:
|
|
25
|
+
kwargs["content"] = content
|
|
26
|
+
if itemprop:
|
|
27
|
+
kwargs["itemprop"] = itemprop
|
|
28
|
+
|
|
29
|
+
return _SelfElem("meta")(**kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@Component
|
|
33
|
+
def MetaCharset():
|
|
34
|
+
# If the attribute is present, its value must be
|
|
35
|
+
# an ASCII case-insensitive match for the string "utf-8",
|
|
36
|
+
# because UTF-8 is the only valid encoding for HTML5 documents.
|
|
37
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#charset
|
|
38
|
+
return safe('<meta charset="utf-8">')
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ..component import _Elem
|
|
2
|
+
|
|
3
|
+
Caption = _Elem("caption")
|
|
4
|
+
Col = _Elem("col")
|
|
5
|
+
Colgroup = _Elem("colgroup")
|
|
6
|
+
Table = _Elem("table")
|
|
7
|
+
Tbody = _Elem("tbody")
|
|
8
|
+
Td = _Elem("td")
|
|
9
|
+
Tfoot = _Elem("tfoot")
|
|
10
|
+
Th = _Elem("th")
|
|
11
|
+
Thead = _Elem("thead")
|
|
12
|
+
Tr = _Elem("tr")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from ..component import _Elem, _HTMLComponent
|
|
2
|
+
from ..escape import safe
|
|
3
|
+
|
|
4
|
+
Blockquote = _Elem("blockquote")
|
|
5
|
+
Dd = _Elem("dd")
|
|
6
|
+
Dl = _Elem("dl")
|
|
7
|
+
Dt = _Elem("dt")
|
|
8
|
+
Div = _Elem("div")
|
|
9
|
+
Figcaption = _Elem("figcaption")
|
|
10
|
+
Figure = _Elem("figure")
|
|
11
|
+
Hr = safe("<hr>")
|
|
12
|
+
Menu = _Elem("menu")
|
|
13
|
+
P = _Elem("p")
|
|
14
|
+
Pre = _Elem("pre")
|
|
15
|
+
Li = _Elem("li")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _ListComp(_HTMLComponent):
|
|
19
|
+
def __getitem__(self, children):
|
|
20
|
+
if isinstance(children, str):
|
|
21
|
+
children = (children,)
|
|
22
|
+
|
|
23
|
+
error_message = "List element children must be <li>"
|
|
24
|
+
for child in children:
|
|
25
|
+
ch = str(child).strip()
|
|
26
|
+
assert ch.startswith("<li") and ch.endswith("</li>"), error_message
|
|
27
|
+
|
|
28
|
+
return super().__getitem__(children)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Ul(_ListComp):
|
|
32
|
+
_html_tag = "ul"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Ol(_ListComp):
|
|
36
|
+
_html_tag = "ol"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .. import html
|
|
2
|
+
from ..component import Component
|
|
3
|
+
from ..escape import safe
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@Component
|
|
7
|
+
def MetaTag(
|
|
8
|
+
*,
|
|
9
|
+
index: bool = False,
|
|
10
|
+
follow: bool = False,
|
|
11
|
+
archive: bool = True,
|
|
12
|
+
snippet: bool = True,
|
|
13
|
+
):
|
|
14
|
+
"""HTML meta tag for preventing robots to index a specific site.
|
|
15
|
+
|
|
16
|
+
noindex: prevents a page from being indexed
|
|
17
|
+
nofollow: prevents links from being crawled
|
|
18
|
+
noarchive: not to store an archived copy of the page
|
|
19
|
+
nosnippet: not include a snippet from the page along with the page's listing in search results
|
|
20
|
+
|
|
21
|
+
See:
|
|
22
|
+
- https://www.robotstxt.org/meta.html
|
|
23
|
+
- https://en.wikipedia.org/wiki/Meta_element#The_robots_attribute
|
|
24
|
+
""" # noqa: E501
|
|
25
|
+
|
|
26
|
+
directives = []
|
|
27
|
+
for directive, novalue in [
|
|
28
|
+
(index, "noindex"),
|
|
29
|
+
(follow, "nofollow"),
|
|
30
|
+
(archive, "noarchive"),
|
|
31
|
+
(snippet, "nosnippet"),
|
|
32
|
+
]:
|
|
33
|
+
if not directive:
|
|
34
|
+
directives.append(novalue)
|
|
35
|
+
|
|
36
|
+
if not directives:
|
|
37
|
+
return safe("")
|
|
38
|
+
|
|
39
|
+
directives_str = ", ".join(directives)
|
|
40
|
+
return html.Meta(name="robots", content=directives_str)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generating robots.txt and meta tags for search engine crawlers. See:
|
|
3
|
+
- https://www.robotstxt.org/
|
|
4
|
+
- https://developers.google.com/search/docs/crawling-indexing/robots/intro
|
|
5
|
+
- https://en.wikipedia.org/wiki/Meta_element#The_robots_attribute
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import enum
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from ..component import Component
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Bot(enum.Enum):
|
|
15
|
+
All = "*"
|
|
16
|
+
Google = "Googlebot"
|
|
17
|
+
Bing = "Bingbot"
|
|
18
|
+
Yahoo = "Slurp"
|
|
19
|
+
Yandex = "Yandex"
|
|
20
|
+
Baidu = "BaiduSpider"
|
|
21
|
+
# https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot/
|
|
22
|
+
DuckDuckGo = "DuckDuckBot"
|
|
23
|
+
# https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started
|
|
24
|
+
Twitter = "Twitterbot"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@Component
|
|
28
|
+
def RobotsTxt(*, children: List["Entry"]):
|
|
29
|
+
return children
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@Component
|
|
33
|
+
def Entry(
|
|
34
|
+
*,
|
|
35
|
+
user_agent: Bot,
|
|
36
|
+
disallow: List[str],
|
|
37
|
+
allow: List[str] = [],
|
|
38
|
+
crawdelay: Optional[int] = None,
|
|
39
|
+
sitemap: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
return [
|
|
42
|
+
UserAgent(agent=user_agent),
|
|
43
|
+
*[Disallow(path=path) for path in disallow],
|
|
44
|
+
*[Allow(path=path) for path in allow],
|
|
45
|
+
CrawDelay(delay=crawdelay) if crawdelay else None,
|
|
46
|
+
Sitemap(url=sitemap) if sitemap else None,
|
|
47
|
+
"\n",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@Component
|
|
52
|
+
def UserAgent(*, agent: Bot):
|
|
53
|
+
return f"User-agent: {agent.value}\n"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@Component
|
|
57
|
+
def Disallow(*, path: str):
|
|
58
|
+
return f"Disallow: {path}\n"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@Component
|
|
62
|
+
def Allow(*, path: str):
|
|
63
|
+
return f"Allow: {path}\n"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@Component
|
|
67
|
+
def CrawDelay(*, delay: int):
|
|
68
|
+
return f"Crawl-delay: {delay}\n"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@Component
|
|
72
|
+
def Sitemap(*, url: str):
|
|
73
|
+
return f"Sitemap: {url}\n"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .component import _ChildrenBase, _HTMLComponentBase, safe
|
|
2
|
+
|
|
3
|
+
Xml10 = safe('<?xml version="1.0" encoding="UTF-8"?>')
|
|
4
|
+
Xml11 = safe('<?xml version="1.1" encoding="UTF-8"?>')
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Comment(_ChildrenBase, _HTMLComponentBase):
|
|
8
|
+
def __str__(self) -> str:
|
|
9
|
+
# Anything inside comments should not be escaped
|
|
10
|
+
children = "".join(str(e) for e in self._children)
|
|
11
|
+
return safe(f"<-- {children} -->")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "compone"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Component framework for Python"
|
|
5
|
+
authors = ["György Kiss <gyorgy@duck.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
package-mode = true
|
|
9
|
+
packages = [{ include = "compone" }]
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = "^3.8"
|
|
13
|
+
markupsafe = "^2.1.5"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|