diff --git a/README.md b/README.md
index 2bb4e46..5732d17 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 0000000..f00888e
Binary files /dev/null and b/docs/_static/images/quick-start.jpeg differ
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"
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__",
]
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(
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/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/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.
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/css_modules.py b/src/pydom/styling/css_modules.py
index 818223a..77ace1c 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,7 +6,9 @@
import cssutils
-from ..utils.functions import random_string
+from ..utils.get_frame import get_frame
+
+from ..utils.functions import random_string, remove_prefix
class CSSClass:
@@ -17,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 = []
@@ -67,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(
@@ -79,7 +77,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 +137,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/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/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 d73bbdc..8d179d8 100644
--- a/src/pydom/types/html/html_element.py
+++ b/src/pydom/types/html/html_element.py
@@ -3,13 +3,13 @@
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):
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
@@ -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]
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/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
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()
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/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/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_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)
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(