├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── docs ├── .DS_Store ├── CNAME ├── README.md ├── advanced.md ├── assets │ ├── extra.css │ └── markupy.svg ├── django.md ├── elements.md ├── flask.md ├── html2markupy.md ├── reusability.md └── starlette.md ├── examples ├── django │ ├── db.sqlite3 │ ├── manage.py │ ├── markupyapp │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ └── markupyapp │ │ │ │ └── base.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ └── markupyproject │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py └── starlette │ └── html_response.py ├── mkdocs.yml ├── pyproject.toml ├── scripts ├── benchmark_big_table.py ├── docs.sh ├── fix.sh ├── performance.py ├── release.sh └── test.sh ├── src ├── markupy │ ├── __init__.py │ ├── _private │ │ ├── attributes │ │ │ ├── __init__.py │ │ │ ├── attribute.py │ │ │ ├── handlers.py │ │ │ ├── html.py │ │ │ └── store.py │ │ ├── html_to_markupy │ │ │ ├── __init__.py │ │ │ ├── cli.py │ │ │ └── parser.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── component.py │ │ │ ├── element.py │ │ │ ├── fragment.py │ │ │ └── view.py │ ├── attributes.py │ ├── elements.py │ ├── exceptions.py │ └── py.typed └── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_attribute_handler.py │ ├── test_attributes.py │ ├── test_attributes_dict.py │ ├── test_attributes_kwargs.py │ ├── test_attributes_obj.py │ ├── test_attributes_selector.py │ ├── test_children.py │ ├── test_cli.py │ ├── test_component.py │ ├── test_django.py │ ├── test_element.py │ ├── test_flask.py │ ├── test_fragment.py │ ├── test_starlette.py │ ├── test_template.py │ └── test_view.py └── uv.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | ruff: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: astral-sh/setup-uv@v2 14 | - run: uv python install 15 | - run: uv sync --all-extras --dev 16 | - run: uv run ruff check --output-format=github ./src 17 | - run: uv run ruff format --diff ./src 18 | 19 | pytest: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: astral-sh/setup-uv@v2 27 | - run: uv python install ${{ matrix.python-version }} 28 | - run: uv sync --all-extras --dev 29 | - run: uv run pytest 30 | 31 | mypy: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: astral-sh/setup-uv@v2 39 | - run: uv python install ${{ matrix.python-version }} 40 | - run: uv sync --all-extras --dev 41 | - run: uv run mypy src 42 | 43 | pyright: 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: astral-sh/setup-uv@v2 51 | - run: uv python install ${{ matrix.python-version }} 52 | - run: uv sync --all-extras --dev 53 | - run: uv run pyright 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | __pycache__/ 3 | dist/ 4 | site/ 5 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Guillaume Granger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witiz/markupy/f0a5bb37078b71eda3df8dd81fe33c02fb8236e3/docs/.DS_Store -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | markupy.witiz.com -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Table of contents:
37 | 42 | 43 | 44 | ``` 45 | 46 | Seems interesting? Try it by yourself with our [online html2markupy converter](https://html2markupy.witiz.com). 47 | 48 | ## Motivation 49 | 50 | Like most Python web developers, we have relied on template engines (Jinja, Django, ...) since forever to generate HTML on the server side. Although this is fine for simple needs, when your site grows bigger, you might start facing some issues: 51 | 52 | - More an more Python code get put into unreadable and untestable macros 53 | 54 | - Extends and includes make it very hard to track required parameters 55 | 56 | - Templates are very permissive regarding typing making it more error prone 57 | 58 | If this is you struggling with templates, then you should definitely give markupy a try! 59 | 60 | ## Inspiration 61 | 62 | markupy started as a fork of [htpy](https://htpy.dev). Even though the two projects are still conceptually very similar, we started markupy in order to support a slightly different syntax to optimize readability, reduce risk of conflicts with variables, and better support for non native html attributes syntax. On top of that, markupy provides a first class support for class based components. 63 | 64 | ## Key Features 65 | 66 | - **Leverage static types:** Use [mypy](https://mypy.readthedocs.io/en/stable/) or [pyright](https://github.com/microsoft/pyright) to type check your code. 67 | 68 | - **Great debugging:** Avoid cryptic stack traces from templates. Use your favorite Python debugger. 69 | 70 | - **Easy to extend:** There is no special way to define template tags/filters. Just call regular functions. 71 | 72 | - **Works with existing Python web framework:** Works great with Django, Flask or any other Python web framework! 73 | 74 | - **Works great with htmx:** markupy makes for a great experience when writing server rendered partials/components. 75 | 76 | - **Create reusable components:** Define components, snippets, complex layouts/pages as regular Python variables or functions. 77 | 78 | - **Familiar concepts from React:** React helped make it popular writing HTML with a programming language. markupy uses a lot of similar constructs. 79 | 80 | - **Customizable:** Use or create 3rd party libraries to leverage the power of markupy 81 | 82 | ## Philosophy 83 | 84 | markupy generates HTML elements and attributes and provide a few helpers. 85 | 86 | markupy does not enforce any particular pattern or style to organize 87 | your pages, components and layouts. That does not mean that markupy cannot be used 88 | to build sophisticated web pages or applications. 89 | 90 | Rather the opposite: you are encouraged the leverage the power of Python to 91 | structure your project. Use modules, classes, functions, decorators, list 92 | comprehension, generators, conditionals, static typing and any other feature of 93 | Python to organize your components. This gives you a lot of power and makes markupy 94 | scale from a small one file project to bigger applications. 95 | 96 | 97 | ## Installation 98 | 99 | [markupy is available on PyPI](https://pypi.org/project/markupy/). You may install the latest version using pip: 100 | 101 | ``` 102 | pip install markupy 103 | ``` 104 | 105 | ## Documentation 106 | 107 | The full documentation is available at [markupy.witiz.com](https://markupy.witiz.com): 108 | 109 | - [Mastering elements](https://markupy.witiz.com/elements/) 110 | - [Advanced usage](https://markupy.witiz.com/advanced/) 111 | - [Reusability with Fragments and Components](https://markupy.witiz.com/reusability/) 112 | - [Integrating with Django](https://markupy.witiz.com/django/) 113 | - [Integrating with Flask](https://markupy.witiz.com/flask/) 114 | - [Integrating with Starlette](https://markupy.witiz.com/starlette/) 115 | - [html2markupy](https://markupy.witiz.com/html2markupy/) -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced usage 2 | 3 | ## Loops and conditions 4 | 5 | ### Looping / iterating over content 6 | 7 | You can pass any iterable such as `list`, `tuple` or `generator` to generate multiple children: 8 | 9 | ```python title="Iterate over a generator" 10 | >>> from markupy.elements import Ul, Li 11 | >>> print(Ul[(Li[letter] for letter in "abc")]) 12 |Hello world!
15 | ``` 16 | 17 | ## Components 18 | 19 | Although markupy intend to remain a generic library to allow you generate HTML, it also provides a powerful support for components in order to build reusable chunks of HTML. 20 | 21 | ### Building your first component 22 | 23 | Let's start by creating a component that renders a [Boostrap card](https://getbootstrap.com/docs/5.3/components/card/). 24 | 25 | #### Components as functions 26 | 27 | Building a function component is a simple as returning elements from a regular python function: 28 | 29 | ```python 30 | def card_component(title:str, content:str) -> View: 31 | return Div(".card")[ 32 | Div(".card-body")[ 33 | H5(".card-title")[ 34 | title 35 | ], 36 | P(".card-text")[ 37 | content 38 | ], 39 | ] 40 | ] 41 | ``` 42 | 43 | !!! note 44 | 45 | In the rest of the documentation, we will mostly focus on class based components that offer more flexibility with the ability to inherit each other but after all, it's also a matter of taste so feel free to experiment and find what works best for you. 46 | 47 | 48 | #### Components as classes 49 | 50 | Building a class component is done by subclassing the built-in `Component` abstract class and implementing the one required `render()` instance method that defines your component structure. 51 | 52 | ```python 53 | from markupy import Component, View 54 | from markupy.elements import Div, H5, P 55 | 56 | class CardComponent(Component): 57 | def render(self) -> View: 58 | return Div(".card")[ 59 | Div(".card-body")[ 60 | H5(".card-title")[ 61 | "Card title" 62 | ], 63 | P(".card-text")[ 64 | "This is my card's content." 65 | ], 66 | ] 67 | ] 68 | ``` 69 | 70 | And then to generate the actual HTML for this component, you just need to instantiate it and make it into a `str`: 71 | 72 | ```python 73 | >>> str(CardComponent()) 74 | ``` 75 | 76 | Note that the component `render()` method needs to return a `View`, which means it can be any of an `Element`, `Fragment` or another `Component`. 77 | 78 | See how this can save you from repeating a lot of code? 79 | But we're not there yet, because right now our card always has the same title and content. 80 | Time to keep improving our component. 81 | 82 | ### Pass data to a class component with constructor 83 | 84 | Let's make our card data dynamic by adding a constructor to our component. Let's say our card is in charge of displaying a `Post` object: 85 | 86 | ```python 87 | from markupy import Component, View 88 | from markupy.elements import Div, H5, P 89 | from my_models import Post 90 | 91 | class PostCardComponent(Component): 92 | def __init__(self, *, post: Post) -> None: 93 | super().__init__() 94 | self.post = post 95 | 96 | def render(self) -> View: 97 | return Div(".card")[ 98 | Div(".card-body")[ 99 | H5(".card-title")[ 100 | self.post.title 101 | ], 102 | P(".card-text")[ 103 | self.post.description 104 | ], 105 | ] 106 | ] 107 | ``` 108 | 109 | ### Components in components 110 | 111 | Usually, cards are displayed as part of a collection. Let's say we have a blog that is managing a list of posts, let's create a new component that would be in charge of displaying a list of cards: 112 | 113 | ```python 114 | from markupy import Component, View 115 | from markupy.elements import Div, H5, P 116 | from my_models import Post 117 | 118 | class PostCardListComponent(Component): 119 | def __init__(self, *, posts: list[Post]) -> None: 120 | super().__init__() 121 | self.posts = posts 122 | 123 | def render(self) -> View: 124 | return Div(".card-group")[ 125 | (PostCardComponent(post=post) for post in self.posts) 126 | ] 127 | ``` 128 | 129 | And that's it, we are looping over a list of posts to generate card components that are added as children of another component. Displaying a list of posts as cards is now super easy: 130 | 131 | ```python 132 | >>> print(PostCardListComponent(posts=my_posts)) 133 | ``` 134 | 135 | ### Passing children to components 136 | 137 | Content can be assigned to component the same way we are doing for Fragments or Elements. 138 | To tell your component where such content needs to be injected when rendering, you need to call the `self.render_content()` reserved method: 139 | 140 | ```python 141 | from markupy import elements as el 142 | from markupy import Component, View 143 | 144 | class Title(Component): 145 | def __init__(self, id: str) -> None: 146 | super().__init__() 147 | self.id = id 148 | 149 | def render(self) -> View: 150 | return el.H1(".title.header", id=self.id)[self.render_content()] 151 | ``` 152 | 153 | Then to use this component: 154 | 155 | ```python 156 | >>> print(Title(id="headline")["hello ", el.I(".star.icon")]) 157 | ``` 158 | 159 | This will render as: 160 | 161 | ```html 162 |Row # |
---|
{{ row }} |
Welcome to our cooking site, {{ user.name }}!
84 | 85 |{{ recipe.description }}
87 | 88 |Say: Hello World!
""" 18 | ) 19 | 20 | 21 | def test_render_nested() -> None: 22 | assert Fragment[Fragment["Hel", "lo "], "World"] == """Hello World""" 23 | 24 | 25 | def test_render_embedded() -> None: 26 | assert ( 27 | P[Fragment["Good ", I["morning"]], " ", I["World"]] 28 | == """Good morning World
""" 29 | ) 30 | 31 | 32 | def test_safe() -> None: 33 | assert Fragment['>"'] == """>"""" 34 | 35 | 36 | def test_iter() -> None: 37 | assert list(Fragment["Hello ", None, I["World"]]) == [ 38 | "Hello ", 39 | "", 40 | "World", 41 | "", 42 | ] 43 | 44 | 45 | def test_element() -> None: 46 | result = Fragment[Div["a"]] 47 | assert result == "