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 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
+
@@ -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,3 @@
1
+ # ruff: noqa: F401
2
+ from .component import Component
3
+ from .escape import escape, safe
@@ -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,8 @@
1
+ from ..component import _Elem, _SelfElem
2
+
3
+ Embed = _SelfElem("embed")
4
+ Iframe = _Elem("iframe")
5
+ Object = _Elem("object")
6
+ Picture = _Elem("picture")
7
+ Portal = _Elem("portal")
8
+ Source = _SelfElem("source")
@@ -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,5 @@
1
+ from ..component import _Elem
2
+
3
+ Details = _Elem("details")
4
+ Dialog = _Elem("dialog")
5
+ Summary = _Elem("summary")
@@ -0,0 +1,4 @@
1
+ from ..component import _Elem
2
+
3
+ Html = _Elem("html")
4
+ Body = _Elem("body")
@@ -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,8 @@
1
+ from ..component import _Elem, _SelfElem
2
+
3
+ Area = _SelfElem("area")
4
+ Audio = _Elem("audio")
5
+ Img = _SelfElem("img")
6
+ Map = _Elem("map")
7
+ Track = _SelfElem("track")
8
+ Video = _Elem("video")
@@ -0,0 +1,4 @@
1
+ from ..component import _Elem
2
+
3
+ Svg = _Elem("svg")
4
+ Math = _Elem("math")
@@ -0,0 +1,5 @@
1
+ from ..component import _Elem
2
+
3
+ Canvas = _Elem("canvas")
4
+ Noscript = _Elem("noscript")
5
+ Script = _Elem("script")
@@ -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,4 @@
1
+ from ..component import _Elem
2
+
3
+ Slot = _Elem("slot")
4
+ Template = _Elem("template")
@@ -0,0 +1,3 @@
1
+ # ruff: noqa: F401
2
+ from .html_meta import MetaTag
3
+ from .robots_txt import Bot, Entry, RobotsTxt
@@ -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,7 @@
1
+ def _is_iterable(content):
2
+ try:
3
+ iter(content)
4
+ except TypeError:
5
+ return False
6
+ else:
7
+ return True
@@ -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"