From 4bf63a87659780508875491f1e7d3d18fbdc0d47 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Thu, 13 Feb 2025 16:40:51 +0200
Subject: [PATCH 01/14] Rename StyleObject to StyleSheet
---
.../standard/transformers/style_transformer.py | 6 +++---
src/pydom/styling/__init__.py | 4 ++--
src/pydom/styling/{style_object.py => stylesheet.py} | 12 ++++++------
src/pydom/types/html/html_element.py | 4 ++--
4 files changed, 13 insertions(+), 13 deletions(-)
rename src/pydom/styling/{style_object.py => stylesheet.py} (78%)
diff --git a/src/pydom/context/standard/transformers/style_transformer.py b/src/pydom/context/standard/transformers/style_transformer.py
index e5dac3d..c39c126 100644
--- a/src/pydom/context/standard/transformers/style_transformer.py
+++ b/src/pydom/context/standard/transformers/style_transformer.py
@@ -1,10 +1,10 @@
-from ....styling import StyleObject
+from ....styling import StyleSheet
from ...transformers import PropertyTransformer
class StyleTransformer(PropertyTransformer):
def match(self, _, value):
- return isinstance(value, StyleObject)
+ return isinstance(value, StyleSheet)
- def transform(self, key: str, value: StyleObject, element):
+ def transform(self, key: str, value: StyleSheet, element):
element.props[key] = value.to_css()
diff --git a/src/pydom/styling/__init__.py b/src/pydom/styling/__init__.py
index c4cfdec..c442839 100644
--- a/src/pydom/styling/__init__.py
+++ b/src/pydom/styling/__init__.py
@@ -1,5 +1,5 @@
from .color import Color
-from .style_object import StyleObject
+from .stylesheet import StyleSheet
from .css_modules import CSS
-__all__ = ["Color", "StyleObject", "CSS"]
\ No newline at end of file
+__all__ = ["Color", "StyleSheet", "CSS"]
\ No newline at end of file
diff --git a/src/pydom/styling/style_object.py b/src/pydom/styling/stylesheet.py
similarity index 78%
rename from src/pydom/styling/style_object.py
rename to src/pydom/styling/stylesheet.py
index 1c1b22a..882c534 100644
--- a/src/pydom/styling/style_object.py
+++ b/src/pydom/styling/stylesheet.py
@@ -6,9 +6,9 @@
T = TypeVar("T")
-class StyleObject:
+class StyleSheet:
class _StyleProperty(Generic[T]):
- def __init__(self, instance: "StyleObject", name: str):
+ def __init__(self, instance: "StyleSheet", name: str):
self.instance = instance
self.name = name.replace("_", "-")
@@ -18,12 +18,12 @@ def __call__(self, value: T):
def __init__(
self,
- *styles: Union["StyleObject", CSSProperties],
+ *styles: Union["StyleSheet", CSSProperties],
**kwargs: Unpack[CSSProperties],
):
self.style: Dict[str, object] = {}
for style in styles:
- if isinstance(style, StyleObject):
+ if isinstance(style, StyleSheet):
style = style.style
self.style.update(style)
self.style.update(kwargs)
@@ -32,7 +32,7 @@ def __init__(
}
def copy(self):
- return StyleObject(self)
+ return StyleSheet(self)
def to_css(self):
return "".join(map(lambda x: f"{x[0]}:{x[1]};", self.style.items()))
@@ -41,4 +41,4 @@ def __str__(self):
return self.to_css()
def __getattr__(self, name: str):
- return StyleObject._StyleProperty(self, name)
+ return StyleSheet._StyleProperty(self, name)
diff --git a/src/pydom/types/html/html_element.py b/src/pydom/types/html/html_element.py
index d73bbdc..e7ce527 100644
--- a/src/pydom/types/html/html_element.py
+++ b/src/pydom/types/html/html_element.py
@@ -3,7 +3,7 @@
from typing_extensions import TypedDict
if TYPE_CHECKING:
- from pydom.styling import StyleObject
+ from pydom.styling import StyleSheet
class HTMLElement(TypedDict, total=False, closed=False):
@@ -21,7 +21,7 @@ class HTMLElement(TypedDict, total=False, closed=False):
lang: Optional[str]
role: Optional[str]
spell_check: Optional[str]
- style: Optional[Union[str, "StyleObject"]]
+ style: Optional[Union[str, "StyleSheet"]]
tab_index: Optional[str]
title: Optional[str]
translate: Optional[str]
From bbe400155b572fa8fe2f9f898c591331b757fd44 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Thu, 13 Feb 2025 21:16:49 +0200
Subject: [PATCH 02/14] Refactor CSSModule to use get_frame utility for
improved frame retrieval
---
src/pydom/styling/css_modules.py | 17 +++++++----------
src/pydom/utils/get_frame.py | 23 +++++++++++++++++++++++
2 files changed, 30 insertions(+), 10 deletions(-)
create mode 100644 src/pydom/utils/get_frame.py
diff --git a/src/pydom/styling/css_modules.py b/src/pydom/styling/css_modules.py
index 818223a..49307be 100644
--- a/src/pydom/styling/css_modules.py
+++ b/src/pydom/styling/css_modules.py
@@ -1,4 +1,3 @@
-import inspect
import re
from os import PathLike
@@ -7,6 +6,8 @@
import cssutils
+from ..utils.get_frame import get_frame
+
from ..utils.functions import random_string
@@ -79,7 +80,7 @@ def __init__(self, module_name):
if rule.type == rule.UNKNOWN_RULE:
self.raw_css += rule.cssText
- def __getattr__(self, __name: str):
+ def __getattr__(self, __name: str) -> str:
if __name not in self.classes:
raise AttributeError(f"CSS class {__name} not found in {self.module_name}")
return self.classes[__name].uuid
@@ -139,14 +140,10 @@ def set_root_folder(cls, folder: Union[PathLike, str]):
def _full_path(cls, css_path: Union[PathLike, str]) -> Path:
if isinstance(css_path, str):
if css_path.startswith("./"):
- frame = inspect.stack()[2]
- module = inspect.getmodule(frame[0])
- if module is None or module.__file__ is None:
- raise ValueError(
- "Cannot use relative path in a module without a file"
- )
-
- css_path = Path(module.__file__).parent / css_path
+ frame = get_frame(2)
+ module = frame.f_globals["__name__"]
+ module_path = Path(module.replace(".", "/"))
+ css_path = module_path.parent / css_path[2:]
css_path = Path(css_path)
diff --git a/src/pydom/utils/get_frame.py b/src/pydom/utils/get_frame.py
new file mode 100644
index 0000000..3809480
--- /dev/null
+++ b/src/pydom/utils/get_frame.py
@@ -0,0 +1,23 @@
+import sys
+from sys import exc_info
+from types import FrameType
+
+
+def get_frame_fallback(n: int):
+ try:
+ raise Exception
+ except Exception:
+ frame: FrameType = exc_info()[2].tb_frame.f_back # type: ignore
+ for _ in range(n):
+ frame = frame.f_back # type: ignore
+ return frame
+
+
+def load_get_frame_function():
+ if hasattr(sys, "_getframe"):
+ return sys._getframe
+
+ return get_frame_fallback
+
+
+get_frame = load_get_frame_function()
From f5f4d01aa78cc6dc0a7ec02c45a25934266fd92b Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Thu, 13 Feb 2025 21:17:04 +0200
Subject: [PATCH 03/14] Add tests for CSS modules
---
tests/css/styles.css | 13 +++++++++++++
tests/test_css_modules.py | 21 +++++++++++++++++++++
2 files changed, 34 insertions(+)
create mode 100644 tests/css/styles.css
create mode 100644 tests/test_css_modules.py
diff --git a/tests/css/styles.css b/tests/css/styles.css
new file mode 100644
index 0000000..a8082fe
--- /dev/null
+++ b/tests/css/styles.css
@@ -0,0 +1,13 @@
+.card {
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: 10px;
+ padding: 20px;
+}
+
+.cardHeader {
+ font-size: 1.5em;
+ font-weight: bold;
+ margin-bottom: 10px;
+}
\ No newline at end of file
diff --git a/tests/test_css_modules.py b/tests/test_css_modules.py
new file mode 100644
index 0000000..30c8d8f
--- /dev/null
+++ b/tests/test_css_modules.py
@@ -0,0 +1,21 @@
+from pydom.styling import CSS
+
+from .base import TestCase
+
+
+class CSSModulesTest(TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ styles = CSS.module("tests/css/styles.css")
+ cls.card_class = styles.card
+ cls.card_header_class = styles.cardHeader
+
+ def test_class_name(self):
+ styles = CSS.module("tests/css/styles.css")
+ self.assertEqual(self.card_class, styles.card)
+ self.assertEqual(self.card_header_class, styles.cardHeader)
+
+ def test_relative_css(self):
+ styles = CSS.module("./css/styles.css")
+ self.assertEqual(styles.card, self.card_class)
+ self.assertEqual(styles.cardHeader, self.card_header_class)
From 6bb8c8b31364d50d7928406ce4f7696dfe166fa9 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Thu, 13 Feb 2025 21:17:42 +0200
Subject: [PATCH 04/14] Rename class_name to classes in ClassTransformer and
related components
---
.../context/standard/transformers/class_transformer.py | 7 +++++--
src/pydom/types/element.py | 2 +-
src/pydom/types/html/html_element.py | 2 +-
src/pydom/types/svg/svg_element_props.py | 2 +-
tests/components/__init__.py | 8 ++++----
tests/simple.py | 2 +-
tests/test_json_rendering.py | 4 ++--
7 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/src/pydom/context/standard/transformers/class_transformer.py b/src/pydom/context/standard/transformers/class_transformer.py
index 67aaf78..eaa7c12 100644
--- a/src/pydom/context/standard/transformers/class_transformer.py
+++ b/src/pydom/context/standard/transformers/class_transformer.py
@@ -2,12 +2,15 @@
class ClassTransformer(PropertyTransformer):
+ def __init__(self, prop_name="classes"):
+ self.prop_name = prop_name
+
def match(self, prop_name, _) -> bool:
- return prop_name == "class_name"
+ return prop_name == self.prop_name
def transform(self, _, prop_value, element):
if not isinstance(prop_value, str):
prop_value = " ".join(prop_value)
element.props["class"] = " ".join(str(prop_value).split()).strip()
- del element.props["class_name"]
+ del element.props[self.prop_name]
diff --git a/src/pydom/types/element.py b/src/pydom/types/element.py
index be5a014..575abec 100644
--- a/src/pydom/types/element.py
+++ b/src/pydom/types/element.py
@@ -4,7 +4,7 @@
class ElementProps(TypedDict, total=False, closed=False):
access_key: str
auto_capitalize: str
- class_name: str
+ classes: str
content_editable: str
dangerously_set_inner_html: str
dir: str
diff --git a/src/pydom/types/html/html_element.py b/src/pydom/types/html/html_element.py
index e7ce527..8d179d8 100644
--- a/src/pydom/types/html/html_element.py
+++ b/src/pydom/types/html/html_element.py
@@ -9,7 +9,7 @@
class HTMLElement(TypedDict, total=False, closed=False):
access_key: Optional[str]
auto_capitalize: Optional[str]
- class_name: Optional[Union[str, Iterable[str]]]
+ classes: Optional[Union[str, Iterable[str]]]
content_editable: Optional[str]
dangerously_set_inner_html: Optional[Dict[Literal["__html"], str]]
# data: Dict[str, str] # add this if needed in the future
diff --git a/src/pydom/types/svg/svg_element_props.py b/src/pydom/types/svg/svg_element_props.py
index f53862c..acb4e3d 100644
--- a/src/pydom/types/svg/svg_element_props.py
+++ b/src/pydom/types/svg/svg_element_props.py
@@ -6,7 +6,7 @@ class SVGElementProps(TypedDict, total=False):
class _SVGElementProps(TypedDict, total=False):
# Attributes also defined in HTMLAttributes
- class_name: Optional[str]
+ classes: Optional[str]
color: Optional[str]
height: Optional[Union[int, str]]
id: Optional[str]
diff --git a/tests/components/__init__.py b/tests/components/__init__.py
index 2aba954..d0e88bc 100644
--- a/tests/components/__init__.py
+++ b/tests/components/__init__.py
@@ -9,7 +9,7 @@ def __init__(self, name, version) -> None:
def render(self):
return Div(
- class_name="plugin",
+ classes="plugin",
)(
f"{self.name} v{self.version}",
)
@@ -21,7 +21,7 @@ def __init__(self, plugins=None) -> None:
def render(self):
return Div(
- class_name="plugin-list",
+ classes="plugin-list",
)(
*[Plugin(plugin.name, plugin.version) for plugin in self.plugins],
)
@@ -30,7 +30,7 @@ def render(self):
class Card(Component):
def render(self):
return Div(
- class_name="card",
+ classes="card",
)(
*self.children,
)
@@ -39,7 +39,7 @@ def render(self):
class CardTitle(Component):
def render(self):
return H3(
- class_name="card-title",
+ classes="card-title",
)(
*self.children,
)
diff --git a/tests/simple.py b/tests/simple.py
index 4293131..c9e7b6d 100644
--- a/tests/simple.py
+++ b/tests/simple.py
@@ -6,7 +6,7 @@
def index():
return Page(title="Hello, world!")(
- Div(class_name=["a", "b"])("Hello, world!")
+ Div(classes=["a", "b"])("Hello, world!")
)
diff --git a/tests/test_json_rendering.py b/tests/test_json_rendering.py
index 8b4bf72..72d4f92 100644
--- a/tests/test_json_rendering.py
+++ b/tests/test_json_rendering.py
@@ -83,14 +83,14 @@ def render(self):
"Hello",
Div(),
id="my-id",
- class_name="my-class",
+ classes="my-class",
)
class MyComponent2(Component):
def render(self):
return Div(
MyComponent(),
- class_name="my-class",
+ classes="my-class",
)
self.assertRenderJson(
From 933c74e1362641d48ee2b39b2abb4048886891a8 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Thu, 13 Feb 2025 23:45:30 +0200
Subject: [PATCH 05/14] Update docstrings for transformer decorators
---
.../rendering/transformers/post_render_transformer.py | 6 ++----
src/pydom/rendering/transformers/property_transformer.py | 8 ++------
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/src/pydom/rendering/transformers/post_render_transformer.py b/src/pydom/rendering/transformers/post_render_transformer.py
index 636c14d..b8c7c4b 100644
--- a/src/pydom/rendering/transformers/post_render_transformer.py
+++ b/src/pydom/rendering/transformers/post_render_transformer.py
@@ -5,12 +5,10 @@
def post_render_transformer(context: Union[Context, None] = None):
"""
- A decorator to register a post-render transformer.
-
- Post-render transformers are functions that take the rendered tree and can modify it in place.
+ A decorator to register a function as a post-render transformer.
Args:
- context: The context to register the transformer with.
+ context: The context to register the transformer with. If not provided, the default context is used.
Returns:
A decorator that takes a transformer function and registers it.
diff --git a/src/pydom/rendering/transformers/property_transformer.py b/src/pydom/rendering/transformers/property_transformer.py
index 5cf5213..620879d 100644
--- a/src/pydom/rendering/transformers/property_transformer.py
+++ b/src/pydom/rendering/transformers/property_transformer.py
@@ -6,17 +6,13 @@ def property_transformer(
matcher: Union[Callable[[str, Any], bool], str], context: Optional[Context] = None
):
"""
- A decorator to register a property transformer.
-
- Transformers are functions that take a key, a value, and the element node object.
-
- After handling the key and value, the transformer should update the element node
- properties in place.
+ A decorator to register a function as a property transformer.
Args:
matcher: A callable that takes a key and a value and returns a boolean
indicating whether the transformer should be applied.
If a string is provided, it is assumed to be a key that should be matched exactly.
+ context: The context to register the transformer in. If not provided, the default context is used.
Returns:
A decorator that takes a transformer function and registers it.
From 1420120a70a5ad612c63a3bc476a43a683a7ee95 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Fri, 14 Feb 2025 14:44:15 +0200
Subject: [PATCH 06/14] Enhance add_post_render_transformer method to accept
optional before and after parameters
---
src/pydom/context/context.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/pydom/context/context.py b/src/pydom/context/context.py
index 3946ba2..c9be61c 100644
--- a/src/pydom/context/context.py
+++ b/src/pydom/context/context.py
@@ -105,13 +105,18 @@ def add_prop_transformer(
self._prop_transformers.insert(index, (matcher, self.inject(transformer)))
def add_post_render_transformer(
- self, transformer: Union[PostRenderTransformerFunction, PostRenderTransformer]
+ self,
+ transformer: Union[PostRenderTransformerFunction, PostRenderTransformer],
+ /,
+ *,
+ before: Optional[List[Type[PostRenderTransformer]]] = None,
+ after: Optional[List[Type[PostRenderTransformer]]] = None,
):
try:
index = self._find_transformer_insertion_index(
self._post_render_transformers,
- before=[PostRenderTransformer],
- after=[PostRenderTransformer],
+ before=before,
+ after=after,
)
except Error as e:
raise Error(
From 2707f4c636861ed2132beaf357b6cb2ba23cc5a5 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:01:26 +0200
Subject: [PATCH 07/14] Simplify add_rule method by using setdefault for
sub_rules
---
src/pydom/styling/css_modules.py | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/pydom/styling/css_modules.py b/src/pydom/styling/css_modules.py
index 49307be..8c75cf5 100644
--- a/src/pydom/styling/css_modules.py
+++ b/src/pydom/styling/css_modules.py
@@ -18,10 +18,7 @@ def __init__(self, class_name: str):
self.uuid = random_string()
def add_rule(self, rule: str, properties: Dict[str, str]):
- if rule not in self.sub_rules:
- self.sub_rules[rule] = {}
-
- self.sub_rules[rule].update(properties)
+ self.sub_rules.setdefault(rule, {}).update(properties)
def to_css_string(self, minified=False):
rules = []
From 4f89b81e0d0e7467e605f6f45f525970cabe8d68 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:06:22 +0200
Subject: [PATCH 08/14] Add Page to module exports in __init__.py
---
src/pydom/__init__.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/pydom/__init__.py b/src/pydom/__init__.py
index 85abf54..52ea974 100644
--- a/src/pydom/__init__.py
+++ b/src/pydom/__init__.py
@@ -1,6 +1,7 @@
from .component import Component
from .context.context import Context, set_default_context
from .html import *
+from .page import Page
from .svg import *
from .rendering import render
from .version import version as __version__
@@ -148,5 +149,6 @@
"Component",
"Context",
"render",
+ "Page",
"__version__",
]
From 1e1f7e5eed065fb72de88e17d49b766b85e1fe3a Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:16:59 +0200
Subject: [PATCH 09/14] Update readme
---
README.md | 99 ++++++++++++++++++++++++++-
docs/_static/images/logo.svg | 25 +++++++
docs/_static/images/quick-start.jpeg | Bin 0 -> 16178 bytes
3 files changed, 123 insertions(+), 1 deletion(-)
create mode 100644 docs/_static/images/logo.svg
create mode 100644 docs/_static/images/quick-start.jpeg
diff --git a/README.md b/README.md
index 2bb4e46..962a036 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,100 @@
# PyDOM
-See [Seamless](https://seamless.rtfd.io) for a more complete and up-to-date implementation of this idea.
+
+
+
+
+
+ *Simple to learn, easy to use, fully-featured UI library for Python*
+
+
+
+
+
+
+
+
+PyDOM is a Python library that allows you to create web pages using a declarative syntax.
+
+PyDOM provides a set of components that represent HTML elements and can be composed to create complex web pages.
+
+## Quick Start
+
+This is a quick start guide to get you up and running with PyDOM. The guide will show you how to setup PyDOM and integrate it with [FastAPI](https://fastapi.tiangolo.com/).
+
+### Installation
+
+First, install the PyDOM package.
+
+```bash
+pip install pydom
+```
+
+### Create Reusable Page
+
+PyDOM provides a default page component that is the minimal structure for a web page.
+
+The page can be customized by extending the default page component and overriding the `head` and the `body` methods.
+
+More information about the default page component can be found [here](#page).
+
+```python
+# app_page.py
+
+from pydom import Link, Page
+
+class AppPage(Page):
+ def head(self):
+ return (
+ *super().head(),
+ Link(
+ rel="stylesheet",
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
+ )
+ )
+```
+
+### Creating the FastAPI app
+
+Lastly, create the `FastAPI` app and add an endpoint that will render the page when the user accesses the root route.
+
+```python
+# main.py
+
+from fastapi import FastAPI
+from fastapi.responses import HTMLResponse
+from pydom import render, Div, P
+
+form app_page import AppPage
+
+app = FastAPI()
+
+@app.get("/", response_class=HTMLResponse)
+async def read_root():
+ return render(
+ AppPage(
+ Div(classes="container mt-5")(
+ Div(classes="text-center p-4 rounded")(
+ Div(classes="display-4")("Hello, World!"),
+ P(classes="lead")("Welcome to PyDOM"),
+ )
+ )
+ )
+ )
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="localhost", port=8000)
+```
+
+That's it! Now you can run the app and access it at [http://localhost:8000/](http://localhost:8000/).
+
+It should display a page like this:
+
+
+
+
+
+## Documentation
+
+The full documentation can be found at [our documentation site](https://pydom.dev/).
diff --git a/docs/_static/images/logo.svg b/docs/_static/images/logo.svg
new file mode 100644
index 0000000..88bb834
--- /dev/null
+++ b/docs/_static/images/logo.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_static/images/quick-start.jpeg b/docs/_static/images/quick-start.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..f00888eece21c911a1ed5c8d12cebdee3ad15b4d
GIT binary patch
literal 16178
zcmeI1c~n!^y1aIM4?A)r9uN;J$M0TKvPt+g@;l&c5?
zgcfSR1VRxogoLCj0U1+>Nth$^80HYBkQZ*>TYYb>d*52`pLgFMd}p0~zVq#G?{A-9
zcD}uHBI}lY1de^}aMc0Wvj+h7$PYj^yr=u>l`B5q+<-e=b%Op;@g9)NzKZ}55*mrQ
z;rP|ro3}j9Dt`Q<#ZKLARK&fV>pu~BzpMP6-T{DX^iO>LC-cEOcOy`8hXwihIYu5_
zK3G+`uIl%Pett*y{X@6j(Q%RYBIQ2c?C2QR8&~A|9l3tq@4x82|3ydL!|eD!mit(T
z1V`<}+L4_RAGsTfaFJgR$j?uK2;c_b2z<5k{PKIbgcSk6WibFK{P?HMHx~e?4*}q0
z@1M5Me*ysI?*ZU7=TF<8Ozz#r-2Qua`{d
z|G@)?4jnvj@Zg~%%0~_zRyus};8D;~CFNtsK*tUpIj(a2n2KCKw$sU;ot6sw4#*oG
zJACl4-1@&1*>3>ofP&ZlErmVWz+TWE1<)Q@1E3+ty;ot6T>kTE0mTFRKH0xVVedhC
z_0?nY`MGb;UIn@7;335wr$25KK?jb1rUL%bSylUX_>f#KYdU3KF_+zQWjD#$$$uhuc>bjph~4d
zqt+G;c21U8D)=A0dCPGjS+o7A)dJCMhm9)F-ZO`EK|DV*cn(6UK(f4*m07>zgmphX
z^!VwD&DV>bUEBFf`fEt7ESkmVH6V)`k0biaCO7BjR;wIeuo_)jh_`|8iqkeGJ(%F@
zI&G#~PreVvp3vrhx}*_xlW!uhoerqCb74p%ICv-673$jN1TS+s^}{qX+HcG&C)-I#nP_NN}y#LTi0yYJLxF&jJQY&(_w;HV#P{yWB3Q4>!h
zuN6kVQ(5wUEjd2!$$B5_MgJfWEgB2@dJy((XD53}ZlNjs#ii%pD5j?u`b=z}p;?U1
zL|x~mLz!19NEuzkIU{d8gB9Gj+%#O(;&V{CPQ&E6P1C(riAe-vg0qs&&;3gRmpW(z
zZJ=(7RL&%^X9h|JVp~BNyF;}mw3v%Haqw)-)}4nl&*3qIBw|kd-dr3*uMNw+q8sB
zla$iRhxYaZbP&|Jw=`W!jJL6Au0&jx(xwa3rQ;i=t92bY{{DiS1=6X<{TUwPw%sL+
z1Oubyb-2b5-kNf?MRK{W9<8Q=*}e{YUzDOVUzpK-!@Q{0FuV&
zmj*V1l1-xXLJ2b9G0`@X
zx_y}Crc{{FOx?ajduP=u=66sr#)POVFSc~xWrES+h_n~|pZ3vI1r~;#FfiDBtHvHD
zUTYw}KrDKu;dB~I9&!BOlS@Z(a|>}VYO_fC-)evRuTHan#@}18*4K{-QM_rXMIGwD
z9%DXoCM?@qmjpTAuz^TuU5PIbH|?AnjO8@*5(`-;?Pl$8+jasO(EWB1y1`yckkYa@
zb3NQDl2SO^30t=lf0YW?WPpzh@YU;yC@7CeJY9dRo)?rD0n!keDJ(A2W~X`U7J)8xVJfN5IDO?>OpB@NIyKju<6fFR
zu!+8d7%i%pl@M6_-5*hs!?#oK8EPQOk$0CqorxR|!wQ&82W-^)WhA9;f{$v4fV2)I
zLG9j}k<&8lSF%P&eVTr&Y>kRS4_0|~BFSc6gg^?Rcm-`GNnV4adjs^j
zlrs{HfPBi*scv}FwfbzMxhp}BpI#`c)w}Sf3eQili5w9UP`$oF(K`M0w1kT=!xqAy
ze7oSxv0zO6L?FkJwad~3r(2pwdIY3g0>wSmt5eO1(-^QM*}P4Z0bh2UkpcZkFBy=!
zbR1`2_gMq}F#A<>_R;2^QQxr%WcMJwvUycPuE*%KLwx+_h15h=UHBu*lsG*ZVBmc=
zk&?NYW9_*8N<%?fp0}B7{g2JwJp~#isR^WzI_U3G&SxWu>vM0%(rin(tZW0_S}F|5
z8b~Hp%vQx&t>~q#xIY*g>SY%jWcu}$nBZR5YsNips~c&@kjRDJ1LM9rV+Vw5|29eC
zT&w+IdbI6!U<|*|;8MjybB35Zn7}b}EfeLvp6@9VlZr^jE^02|xtyH&}VWLhXze;ag?)q<}kEZgH@+!jcplf0KqSJO`0%HaENdxX(GQ@=a53
z(2_i54v#&in|0Idn_jr`oEjRok2mjei#ve66CQ73HF>>dG+wWSk*#;;baL+eiUALX
z^%D&BCXP2RLS4mrWzV%1q!|!zb2Ojn$tbe%z4$9?Ig+=Ez)*zo7ul^vc=wTt6PN7F
z$kPSi1-ma0BP#tE2@rVLoL{7paJ_Bu07JOP=2AG6R
z=y8y#v4F)5{Fv0z#{B$NS%7h{ICoD0Pe$R)AoVURnM
z5zr)3bL`MYpB>~jLJQsqJ_dn}KF<6)JAVo~H)#Ku}c|&B>&UzkHQoP-jM&kXpBo
z)m>Bl7-6dySpND+PzL;OQZm1oGfF7t2avRVeO-#b{BUFz=J&~{)sg9!C0bhUS(s7Y
z+>$(*XO>v}Zd@$j6{Lr!$0uayUg!EiZ@DI#OTl9_Eg9gfU-Pl5L^$@y6`$R_Zj&10
zK{0AujVNiP!SNT`>ap=Jrjl0(FNrxx>oG4btsQ&U%wAx#=kBnbPeo*9q4m>+1X@B}
z>6Vp~>Gu8$L{sSv(uMTp4=`5D8b|POv8=-leZELn_d-(6O7d(u^0YMEk|$MmmjU)=
zb=8q!;)WUb6&oMv@ywv1;R?He50qm07VPbK;q63aKpJO+;
zgDzjV{{5}cpG(ufR#QuCnA7xjObWg4U73m>qgydv(vYf7T>IEl;@k|}aJ90oHd@zL
z7u7G0R12frjKML2BNg@eM$@ym-VCx^pLRl^8Y1UD#%3|Aw>rbhzRjrtml65PkfGoV
z-!^AzqnQhn0We7xj1j^==N3>88YG`U@dw^-22d^(zVWt14L|Vvy*$*!k>MU_PC#~~
zB)xqz`uZYebcTBJL%b)>cdQN_P|@0}Kd+^<+#CCL09;kmD}A2Z*!^*2%%d=#&L6Jx
zTgc)bQ<`r&@fY31b1OoXAEOi!@m9XmD|k8&)>|?#PDg^Fs#_C`HpjOvjtV_XyHthv0Sm_5Y;&i#Uu7(*rCRIIv
zs$$oZgjUoS47sZqXpyAxw9GX5Qc8P@2qjGteCy&L;~O|k+mQx0U4#q(#VfM>PK#Qx
z*q$m(oqsMdX;o!DM`D<7?--MmG(Z2JXu~Z1PL5jAxJW(sjVQ*{;I=N<;&RQc)u-E8
zd6_ezV|F2CP>2?W5joRPF!OGkrS(g}h3eRlP`77E4g4jBmjTT~N-@)XHccpJBb*Mb-V*gkm^&hNtE2p;rOFqADVJ{LNe-RzVX3K>iFj7%KE(;
z%aFU9SwfoaG`g-d<$U_~_Og)r_Ey2^4VQMa*o3S6xeAXOzr1DqBw@jy%(+h9pD|qN
zTk*I7j%R-8tN7ith+2hCO(MNPJ-qo}@w~>%OPPbM9}5KNLh%OlLUZ9l0TGL91SQ
zs0c_19xNIlm|-u>y_&dPjc<#S+9IDhonte|
z9jQ&YNA>BS@>s&)+K$F{yBUuGCRv-esn*2I_*$1h)LXAI)+@ESHL7VGUh<2E0(Z`E
zF(j)c1alpg_b5U|ldqQ`?r9>9x*%J#97_^9K`M(LWpidt*lcFztKVmq<{Gfe!p5RH
zdi?T{8FfRH2eHG3VB|@(a)GL{!Jt+9Gb(1};d~V0?HZGsJUI)^KYINf!wIi51XXQt
zTsv#B?{e-li1+gW!
zUQtlUqV|5+9ZwoUMU~f^Jl`AN^$Kof506foKYvzP?fK{9aYdJ@?i;W`UI&iLXsNmw
zK2vBI3=T?4U8}OQEV8j3TwJ^z9;Q)$yhE^QjLWJX^4#)>qiw{$el4{KM#_It6N-|F
zqc{n+KnC0~3_?C{ir(&8xf5I+eF_(_k}^NyjC5XWGbZXM-rD$?xd`QoWPtJ{pB(k@
z-TB{GiMW=f1#)AF_COsvA^B10C=_QvA;ltGxggN;+_ShdC?R{H{WtJZuvZl}4FN)&
zxIhRb#&VeFF7?#}@@i`W2TYxvuqgrQogj&wGYHhHQ+=lKRL9SuucNaYCpq6l!xid(
zv+!&l|3U>dRT}{cJ8u1fJBVtv4VZU@hb~U3*l?ZT(aGzq6X3;pcWCQtBLR{&4@J1B
zm~wiS;z9rRuU!)ULG=9@d3WdTT`u6-Wgwh_!Qo^UTEEC(83r0B#KJqYQH$3wfz8x2
zZ%&C-NN1Gneyi>;Oh-9C$*!WKbVHnC=(qcdFqm;|Tidt1-T)bpK(Y;e6w&-@ax_2I
zh5WwiKNUt^9!n-KMz7N
zIyyj1jdZq$x$7@+L8@F8NMAqN#B*KP)0d7GIIm^lt@2amys^d_^7ju=Q7*Zqk%jC@
z5<%6C`-r0ELR0w%Ua&nmw-G>-0cwLI<34XXSfTff+Gq2=$gxeXvanVm|O9QV4Igo$U*rLKK>E?7RS%`i2(2c)`AG!&AOsQ6NOCUF};Af~{5
z40uREYJPYDG&MW7qJtjz_&{z;iPiB{wN};D`Ekm?#(4@M7Qarka*DfVxRiHhsBOwJ
ztszFKb;+U1PH)PLjkxvrd4hx4DbpquP?p;rE8~N7@pSOL~+Ap;2cpqV9TO1B^u+`Y=
zPy4=~Q*&Z=xSMxZ*qWEfA)g+ot8fMlYNJUy)f;efNU<>R3teU!G21*pKX96qVd`Sa
zgGF)zOG$(x{i4#G9714HPL2q?cjm7;<>eh=@4wgb
ztnln8f8DrX4xtVS^?p~AmZ+YW6%#;ne3e8b1>|O!I^jffS^3$?FHhccf1OVy%wav|
zE--3}`GF?V<8x7$SMGSfZ!W3sFCqlZLrq~~fc}H8@c#po7GWx7ij-2MtDl^lobX*zB5}j8Z!0_Gr@o-M&Wd66OQzjwNMc-e
zX*2Bvx-?g6%nxWPVZ9Bc*oHWyreTotN@E00@fd0%K%5mG5+B6{VMKHY#LUI4f$iOu
zad!s1!#~kgw5B;yw#wNQTL~i#8pEsmQ?0I1SUk
zp{`sNjgp%1K|N~OwW*5Ly2Ej(Cv)IecAw8g;8gEA-6B;K3vk*jKP<`DYCYDzDPgd{
zf1T)0%qduOvX?eAU`4%F#uFe2+NZ;0;8fVUd0|M+h}SX_GneqdIs1v9zKDSz2hX{q
zTICyI!zO}nf!G-WniA=a8OAdO%6W4b3x=EV=*PeMo1a`i71>Kk3G>Zd$pn7wNt%
zX?ESLw-yE>q0=_Ob5MR<}U0dTAf!J5wkGu~uv>#u>Rx5d8_$R23(v
zGgw^Az+M~pftl!;7^Am=T9ImT=^{N5E7FmDz6*2x3b`Kcy6!E}^Tc3u6ip7Rx2p#&
zwK;j^MZca-9~jrQ9+bH72*jonP2Gsly0VX|eg+z4^%eRNK
zd)rOhuuDz;Ygn7XAM0vUt1j!(v!Z-rvR>CSD+BrTLwOPRYun#YBls_KNQPcTd4*BP
zZKv&@KL)KiYJ9*vH~xg-<$VfoBGqJrVBm)cNT{?{{*MIaB}r%f!;K%A0!UkQhcK)E
z9dFx(UHLPq!knWSK26$obwQZ9gyt1Q6I1l6+0Ld2h{}{{<FDp~zL6Rd`pPySJRdu-C|tDbg~nBGI&&i9w$
zM@Bwvz4FWZ*48#Yx48WsF*$`e7R6h!s&Qm$r}Em>;BWJ|t{dOX*0N5MA4k}rUnSEL
z48vzC6ZQRbQhU@!JCQBp@tp`nI|93TL(5+@r|t!wJW~4qAbIT$VVA%zfn5T-1a=AR
z64)iMOJJA4E`ePFy99O#>=M`|uuEW}5d1QcXF
F{|<=tPpkj{
literal 0
HcmV?d00001
From a532789be016c84e424142165407272abf6a7962 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:18:37 +0200
Subject: [PATCH 10/14] Update image links in README
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 962a036..b33cee0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# PyDOM
-
+
@@ -92,7 +92,7 @@ That's it! Now you can run the app and access it at [http://localhost:8000/](htt
It should display a page like this:
-
+
## Documentation
From f175fa40bc841a315f33da9a716ef61c2ab3a265 Mon Sep 17 00:00:00 2001
From: Neriya Cohen <42520786+neriyaco@users.noreply.github.com>
Date: Wed, 26 Feb 2025 15:21:35 +0200
Subject: [PATCH 11/14] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b33cee0..b6f8b43 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- *Simple to learn, easy to use, fully-featured UI library for Python*
+ Simple to learn, easy to use, fully-featured UI library for Python
From 26b0aa324729cd79540b2bab5fa13f1db25ebc76 Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:31:11 +0200
Subject: [PATCH 12/14] Rename package from python-dom to pydom and update
project metadata in pyproject.toml
---
pyproject.toml | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 16f8cd0..b65f5c1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,22 @@
[project]
-name = "python-dom"
+name = "pydom"
authors = [{ name = "Xpo Development", email = "dev@xpo.dev" }]
description = "A Python package for creating and manipulating reusable HTML components"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
- "Programming Language :: Python :: 3",
+ "Development Status :: 4 - Beta",
+ "Environment :: Web Environment",
+ "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
]
dynamic = ["dependencies", "version"]
@@ -28,3 +37,6 @@ dependencies = { file = ["requirements.txt"] }
[tool.setuptools.package-data]
pydom = ["py.typed"]
+
+[project.scripts]
+pydom = "pydom.cli:main"
From a23e30968fd548edb8ee0fa91a1d8ba98f2231db Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:32:02 +0200
Subject: [PATCH 13/14] Update image links in README
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index b6f8b43..5732d17 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# PyDOM
-
+
@@ -92,7 +92,7 @@ That's it! Now you can run the app and access it at [http://localhost:8000/](htt
It should display a page like this:
-
+
## Documentation
From 5e591d932fec0b19ed307c9372f8e1f34852d81f Mon Sep 17 00:00:00 2001
From: Neriya Cohen
Date: Wed, 26 Feb 2025 15:47:11 +0200
Subject: [PATCH 14/14] Add remove_prefix function to support python 3.8
---
src/pydom/styling/css_modules.py | 4 ++--
src/pydom/utils/functions.py | 8 ++++++++
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/pydom/styling/css_modules.py b/src/pydom/styling/css_modules.py
index 8c75cf5..77ace1c 100644
--- a/src/pydom/styling/css_modules.py
+++ b/src/pydom/styling/css_modules.py
@@ -8,7 +8,7 @@
from ..utils.get_frame import get_frame
-from ..utils.functions import random_string
+from ..utils.functions import random_string, remove_prefix
class CSSClass:
@@ -65,7 +65,7 @@ def __init__(self, module_name):
css_property.name: css_property.value for css_property in rule.style
}
- base_name = first_selector.seq[0].value.removeprefix(".")
+ base_name = remove_prefix(first_selector.seq[0].value, ".")
css_class = self.classes.get(base_name, CSSClass(base_name))
for selector in selectors:
css_class.add_rule(
diff --git a/src/pydom/utils/functions.py b/src/pydom/utils/functions.py
index f58f25c..e839a43 100644
--- a/src/pydom/utils/functions.py
+++ b/src/pydom/utils/functions.py
@@ -31,3 +31,11 @@ def flatten(iterable):
yield from flatten(item)
else:
yield item
+
+
+def remove_prefix(text: str, prefix: str) -> str:
+ if hasattr(str, "removeprefix"):
+ return text.removeprefix(prefix)
+ if text.startswith(prefix):
+ return text[len(prefix) :]
+ return text