Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,100 @@
# PyDOM

See [Seamless](https://seamless.rtfd.io) for a more complete and up-to-date implementation of this idea.
<p align="center">
<img src="https://raw.githubusercontent.com/xpodev/pydom/refs/heads/main/docs/_static/images/logo.svg" alt="pydom-logo" width="200">
</p>

<p align="center">
<strong>Simple to learn, easy to use, fully-featured UI library for Python</strong>
</p>

<p align="center">
<a href="https://pypi.org/project/python-dom/">
<img src="https://img.shields.io/pypi/v/pydom.svg" alt="PyPI version">
</a>
</p>

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:

<p align="center">
<img src="https://raw.githubusercontent.com/xpodev/pydom/refs/heads/main/docs/_static/images/quick-start.jpeg" alt="Quick Start">
</p>

## Documentation

The full documentation can be found at [our documentation site](https://pydom.dev/).
25 changes: 25 additions & 0 deletions docs/_static/images/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/images/quick-start.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]

Expand All @@ -28,3 +37,6 @@ dependencies = { file = ["requirements.txt"] }

[tool.setuptools.package-data]
pydom = ["py.typed"]

[project.scripts]
pydom = "pydom.cli:main"
2 changes: 2 additions & 0 deletions src/pydom/__init__.py
Original file line number Diff line number Diff line change
@@ -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__
Expand Down Expand Up @@ -148,5 +149,6 @@
"Component",
"Context",
"render",
"Page",
"__version__",
]
11 changes: 8 additions & 3 deletions src/pydom/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions src/pydom/context/standard/transformers/class_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
6 changes: 3 additions & 3 deletions src/pydom/context/standard/transformers/style_transformer.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 2 additions & 4 deletions src/pydom/rendering/transformers/post_render_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 2 additions & 6 deletions src/pydom/rendering/transformers/property_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/pydom/styling/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
__all__ = ["Color", "StyleSheet", "CSS"]
26 changes: 10 additions & 16 deletions src/pydom/styling/css_modules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import inspect
import re

from os import PathLike
Expand All @@ -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:
Expand All @@ -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 = []
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("_", "-")

Expand All @@ -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)
Expand All @@ -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()))
Expand All @@ -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)
2 changes: 1 addition & 1 deletion src/pydom/types/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading