diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 0c53b1a..b9660df 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,6 +1,12 @@
name: Test
-on: [push]
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
jobs:
coverage:
diff --git a/.gitignore b/.gitignore
index b66252c..c9676bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# --- DOCS ---
+
+docs/site
+
# --- JAVASCRIPT BUNDLES ---
reactpy_router/bundle.js
diff --git a/README.md b/README.md
index b182609..63aeaab 100644
--- a/README.md
+++ b/README.md
@@ -2,60 +2,4 @@
A URL router for ReactPy
-# Installation
-
-Use `pip` to install this package:
-
-```bash
-pip install reactpy-router
-```
-
-For a developer installation from source be sure to install [NPM](https://www.npmjs.com/) before running:
-
-```bash
-git clone https://github.com/reactive-python/reactpy-router
-cd reactpy-router
-pip install -e . -r requirements.txt
-```
-
-# Running the Tests
-
-To run the tests you'll need to install [Chrome](https://www.google.com/chrome/). Then you
-can download the [ChromeDriver](https://chromedriver.chromium.org/downloads) and add it to
-your `PATH`. Once that's done, simply `pip` install the requirements:
-
-```bash
-pip install -r requirements.txt
-```
-
-And run the tests with `pytest`:
-
-```bash
-pytest tests
-```
-
-You can run the tests in headless mode (i.e. without opening the browser):
-
-```bash
-pytest tests
-```
-
-You'll need to run in headless mode to execute the suite in continuous integration systems
-like GitHub Actions.
-
-# Releasing This Package
-
-To release a new version of reactpy-router on PyPI:
-
-1. Install [`twine`](https://twine.readthedocs.io/en/latest/) with `pip install twine`
-2. Update the `version = "x.y.z"` variable in `reactpy-router/__init__.py`
-3. `git` add the changes to `__init__.py` and create a `git tag -a x.y.z -m 'comment'`
-4. Build the Python package with `python setup.py sdist bdist_wheel`
-5. Check the build artifacts `twine check --strict dist/*`
-6. Upload the build artifacts to [PyPI](https://pypi.org/) `twine upload dist/*`
-
-To release a new version of `reactpy-router` on [NPM](https://www.npmjs.com/):
-
-1. Update `js/package.json` with new npm package version
-2. Clean out prior builds `git clean -fdx`
-3. Install and publish `npm install && npm publish`
+Read the docs: https://reactive-python.github.io/reactpy-router
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
new file mode 100644
index 0000000..54a4f8c
--- /dev/null
+++ b/docs/mkdocs.yml
@@ -0,0 +1,55 @@
+site_name: ReactPy Router
+docs_dir: src
+repo_url: https://github.com/reactive-python/reactpy-router
+
+nav:
+ - Home: index.md
+ - Usage: usage.md
+ - Tutorials:
+ - Simple Application: tutorials/simple-app.md
+ - Custom Router: tutorials/custom-router.md
+ - Reference: reference.md
+ - Contributing: contributing.md
+ - Source Code: https://github.com/reactive-python/reactpy-router
+
+theme:
+ name: material
+ logo: assets/logo.svg
+ favicon: assets/logo.svg
+ palette:
+ # Palette toggle for light mode
+ - scheme: default
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ primary: black
+ accent: light-blue
+
+ # Palette toggle for dark mode
+ - scheme: slate
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+ primary: black
+ accent: light-blue
+
+
+plugins:
+- search
+- mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: ["../"]
+ import:
+ - https://reactpy.dev/docs/objects.inv
+ - https://installer.readthedocs.io/en/stable/objects.inv
+
+markdown_extensions:
+ - admonition
+ - pymdownx.details
+ - pymdownx.superfences
+
+watch:
+ - "../reactpy_router"
+
diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg
new file mode 100644
index 0000000..312fb87
--- /dev/null
+++ b/docs/src/assets/logo.svg
@@ -0,0 +1,160 @@
+
+
+
+
diff --git a/docs/src/contributing.md b/docs/src/contributing.md
new file mode 100644
index 0000000..959ba6e
--- /dev/null
+++ b/docs/src/contributing.md
@@ -0,0 +1,70 @@
+# Contributing
+
+!!! note
+
+ The [Code of Conduct](https://github.com/reactive-python/reactpy/blob/main/CODE_OF_CONDUCT.md)
+ applies in all community spaces. If you are not familiar with our Code of Conduct policy,
+ take a minute to read it before making your first contribution.
+
+The ReactPy team welcomes contributions and contributors of all kinds - whether they
+come as code changes, participation in the discussions, opening issues and pointing out
+bugs, or simply sharing your work with your colleagues and friends. We’re excited to see
+how you can help move this project and community forward!
+
+## Everyone Can Contribute!
+
+Trust us, there’s so many ways to support the project. We’re always looking for people who can:
+
+- Improve our documentation
+- Teach and tell others about ReactPy
+- Share ideas for new features
+- Report bugs
+- Participate in general discussions
+
+Still aren’t sure what you have to offer? Just [ask us](https://github.com/reactive-python/reactpy-router/discussions) and we’ll help you make your first contribution.
+
+## Development Environment
+
+For a developer installation from source be sure to install
+[NPM](https://www.npmjs.com/) before running:
+
+```bash
+git clone https://github.com/reactive-python/reactpy-router
+cd reactpy-router
+pip install -e . -r requirements.txt
+```
+
+This will install an ediable version of `reactpy-router` as well as tools you'll need
+to work with this project.
+
+Of particular note is [`nox`](https://nox.thea.codes/en/stable/), which is used to
+automate testing and other development tasks.
+
+## Running the Tests
+
+```bash
+nox -s test
+```
+
+You can run the tests with a headed browser.
+
+```bash
+nox -s test -- --headed
+```
+
+## Releasing This Package
+
+To release a new version of reactpy-router on PyPI:
+
+1. Install [`twine`](https://twine.readthedocs.io/en/latest/) with `pip install twine`
+2. Update the `version = "x.y.z"` variable in `reactpy-router/__init__.py`
+3. `git` add the changes to `__init__.py` and create a `git tag -a x.y.z -m 'comment'`
+4. Build the Python package with `python setup.py sdist bdist_wheel`
+5. Check the build artifacts `twine check --strict dist/*`
+6. Upload the build artifacts to [PyPI](https://pypi.org/) `twine upload dist/*`
+
+To release a new version of `reactpy-router` on [NPM](https://www.npmjs.com/):
+
+1. Update `js/package.json` with new npm package version
+2. Clean out prior builds `git clean -fdx`
+3. Install and publish `npm install && npm publish`
diff --git a/docs/src/index.md b/docs/src/index.md
new file mode 100644
index 0000000..351fd71
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,18 @@
+# ReactPy Router
+
+A URL router for [ReactPy](https://reactpy.dev).
+
+!!! note
+
+ If you don't already know the basics of working with ReactPy, you should
+ [start there](https://reactpy.dev/docs/guides/getting-started/index.html).
+
+## Installation
+
+Use `pip` to install this package:
+
+```bash
+pip install reactpy-router
+```
+
+[installer.records][]
diff --git a/docs/src/reference.md b/docs/src/reference.md
new file mode 100644
index 0000000..aabc9b3
--- /dev/null
+++ b/docs/src/reference.md
@@ -0,0 +1,5 @@
+# Reference
+
+::: reactpy_router.core
+::: reactpy_router.simple
+::: reactpy_router.types
diff --git a/docs/src/tutorials/custom-router.md b/docs/src/tutorials/custom-router.md
new file mode 100644
index 0000000..fa03675
--- /dev/null
+++ b/docs/src/tutorials/custom-router.md
@@ -0,0 +1,3 @@
+# Custom Router
+
+Under construction 🚧
diff --git a/docs/src/tutorials/simple-app.md b/docs/src/tutorials/simple-app.md
new file mode 100644
index 0000000..5f7fcbd
--- /dev/null
+++ b/docs/src/tutorials/simple-app.md
@@ -0,0 +1,277 @@
+# Simple Application
+
+Let's build a simple web application for viewing messages between several people.
+
+For the purposes of this tutorial we'll be working with the following data:
+
+```python
+message_data = [
+ {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
+ {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
+ {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
+ {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
+ {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
+ {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
+ {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
+ {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
+]
+```
+
+In a more realistic application this data would be stored in a database, but for this
+tutorial we'll just keep it in memory.
+
+## Basic Routing
+
+The first step is to create a basic router that will display the home page when the
+user navigates to the root of the application, and a "missing link" page for any other
+route:
+
+```python
+from reactpy import component, html, run
+from reactpy_router import route, simple
+
+@component
+def root():
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+
+run(root)
+```
+
+When navigating to http://127.0.0.1:8000 you should see "Home Page 🏠". However, if you
+go to any other route (e.g. http://127.0.0.1:8000/missing) you will instead see the
+"Missing Link 🔗💥" page.
+
+With this foundation you can start adding more routes:
+
+```python
+from reactpy import component, html, run
+from reactpy_router import route, simple
+
+@component
+def root():
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("/messages", html.h1("Messages 💬")),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+
+run(root)
+```
+
+With this change you can now also go to `/messages` to see "Messages 💬" displayed.
+
+## Route Links
+
+Instead of using the standard `` element to create links to different parts of your
+application, use `reactpy_router.link` instead. When users click links constructed using
+`reactpy_router.link`, instead of letting the browser navigate to the associated route,
+ReactPy will more quickly handle the transition by avoiding the cost of a full page
+load.
+
+```python
+from reactpy import component, html, run
+from reactpy_router import link, route, simple
+
+@component
+def root():
+ return simple.router(
+ route("/", home()),
+ route("/messages", html.h1("Messages 💬")),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+
+@component
+def home():
+ return html.div(
+ html.h1("Home Page 🏠"),
+ link("Messages", to="/messages"),
+ )
+
+run(root)
+```
+
+Now, when you go to the home page, you can click the link to go to `/messages`.
+
+## Nested Routes
+
+Routes can be nested in order to construct more complicated application structures:
+
+```python
+from reactpy import component, html, run
+from reactpy_router import route, simple, link
+
+message_data = [
+ {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
+ {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
+ {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
+ {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
+ {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
+ {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
+ {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
+ {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
+]
+
+@component
+def root():
+ return simple.router(
+ route("/", home()),
+ route(
+ "/messages",
+ all_messages(),
+ # we'll improve upon these manually created routes in the next section...
+ route("/with/Alice", messages_with("Alice")),
+ route("/with/Alice-Bob", messages_with("Alice", "Bob")),
+ ),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+
+@component
+def home():
+ return html.div(
+ html.h1("Home Page 🏠"),
+ link("Messages", to="/messages"),
+ )
+
+@component
+def all_messages():
+ last_messages = {
+ ", ".join(msg["with"]): msg
+ for msg in sorted(message_data, key=lambda m: m["id"])
+ }
+ return html.div(
+ html.h1("All Messages 💬"),
+ html.ul(
+ [
+ html.li(
+ {"key": msg["id"]},
+ html.p(
+ link(
+ f"Conversation with: {', '.join(msg['with'])}",
+ to=f"/messages/with/{'-'.join(msg['with'])}",
+ ),
+ ),
+ f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
+ )
+ for msg in last_messages.values()
+ ]
+ ),
+ )
+
+@component
+def messages_with(*names):
+ names = set(names)
+ messages = [msg for msg in message_data if set(msg["with"]) == names]
+ return html.div(
+ html.h1(f"Messages with {', '.join(names)} 💬"),
+ html.ul(
+ [
+ html.li(
+ {"key": msg["id"]},
+ f"{msg['from'] or 'You'}: {msg['message']}",
+ )
+ for msg in messages
+ ]
+ ),
+ )
+
+run(root)
+```
+
+## Route Parameters
+
+In the example above we had to manually create a `messages_with(...)` component for each
+conversation. This would be better accomplished by defining a single route that declares
+["route parameters"](../usage.md#simple-router) instead.
+
+Any parameters that have matched in the currently displayed route can then be consumed
+with the `use_params` hook which returns a dictionary mapping the parameter names to
+their values. Note that parameters with a declared type will be converted to is in the
+parameters dictionary. So for example `/my/route/{my_param:float}` would match
+`/my/route/3.14` and have a parameter dictionary of `{"my_param": 3.14}`.
+
+If we take this information and apply it to our growing example application we'd
+substitute the manually constructed `/messages/with` routes with a single
+`/messages/with/{names}` route:
+
+```python
+from reactpy import component, html, run
+from reactpy_router import route, simple, link
+from reactpy_router.core import use_params
+
+message_data = [
+ {"id": 1, "with": ["Alice"], "from": None, "message": "Hello!"},
+ {"id": 2, "with": ["Alice"], "from": "Alice", "message": "How's it going?"},
+ {"id": 3, "with": ["Alice"], "from": None, "message": "Good, you?"},
+ {"id": 4, "with": ["Alice"], "from": "Alice", "message": "Good, thanks!"},
+ {"id": 5, "with": ["Alice", "Bob"], "from": None, "message": "We meeting now?"},
+ {"id": 6, "with": ["Alice", "Bob"], "from": "Alice", "message": "Not sure."},
+ {"id": 7, "with": ["Alice", "Bob"], "from": "Bob", "message": "I'm here!"},
+ {"id": 8, "with": ["Alice", "Bob"], "from": None, "message": "Great!"},
+]
+
+@component
+def root():
+ return simple.router(
+ route("/", home()),
+ route(
+ "/messages",
+ all_messages(),
+ route("/with/{names}", messages_with()), # note the path param
+ ),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+
+@component
+def home():
+ return html.div(
+ html.h1("Home Page 🏠"),
+ link("Messages", to="/messages"),
+ )
+
+@component
+def all_messages():
+ last_messages = {
+ ", ".join(msg["with"]): msg
+ for msg in sorted(message_data, key=lambda m: m["id"])
+ }
+ return html.div(
+ html.h1("All Messages 💬"),
+ html.ul(
+ [
+ html.li(
+ {"key": msg["id"]},
+ html.p(
+ link(
+ f"Conversation with: {', '.join(msg['with'])}",
+ to=f"/messages/with/{'-'.join(msg['with'])}",
+ ),
+ ),
+ f"{'' if msg['from'] is None else '🔴'} {msg['message']}",
+ )
+ for msg in last_messages.values()
+ ]
+ ),
+ )
+
+@component
+def messages_with():
+ names = set(use_params()["names"].split("-")) # and here we use the path param
+ messages = [msg for msg in message_data if set(msg["with"]) == names]
+ return html.div(
+ html.h1(f"Messages with {', '.join(names)} 💬"),
+ html.ul(
+ [
+ html.li(
+ {"key": msg["id"]},
+ f"{msg['from'] or 'You'}: {msg['message']}",
+ )
+ for msg in messages
+ ]
+ ),
+ )
+
+run(root)
+```
diff --git a/docs/src/usage.md b/docs/src/usage.md
new file mode 100644
index 0000000..0bf0387
--- /dev/null
+++ b/docs/src/usage.md
@@ -0,0 +1,164 @@
+# Usage
+
+!!! note
+
+ The sections below assume you already know the basics of [ReacPy](https://reactpy.dev).
+
+Here you'll learn the various features of `reactpy-router` and how to use them. All examples
+will utilize the [simple.router][reactpy_router.simple.router] (though you can [use your own](#custom-routers)).
+
+## Routers and Routes
+
+The [simple.router][reactpy_router.simple.router] component is one possible
+implementation of a [Router][reactpy_router.types.Router]. Routers takes a series of
+[Route][reactpy_router.types.Route] objects as positional arguments and render whatever
+element matches the current location. For convenience, these `Route` objects are created
+using the [route][reactpy_router.route] function.
+
+!!! note
+
+ The current location is determined based on the browser's current URL and can be found
+ by checking the [use_location][reactpy.backend.hooks.use_location] hook.
+
+Here's a basic example showing how to use `simple.router` with two routes:
+
+```python
+from reactpy import component, html, run
+from reactpy_router import route, simple, use_location
+
+@component
+def root():
+ location = use_location()
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("*", html.h1("Missing Link 🔗💥")),
+ )
+```
+
+Here we'll note some special syntax in the route path for the second route. The `*` is a
+wildcard that will match any path. This is useful for creating a "404" page that will be
+shown when no other route matches.
+
+### Simple Router
+
+The syntax for declaring routes with the [simple.router][reactpy_router.simple.router]
+is very similar to the syntax used by [Starlette](https://www.starlette.io/routing/) (a
+popular Python web framework). As such route parameters are declared using the following
+syntax:
+
+```
+/my/route/{param}
+/my/route/{param:type}
+```
+
+In this case, `param` is the name of the route parameter and the optionally declared
+`type` specifies what kind of parameter it is. The available parameter types and what
+patterns they match are are:
+
+- str (default) - `[^/]+`
+- int - `\d+`
+- float - `\d+(\.\d+)?`
+- uuid - `[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`
+- path - `.+`
+
+!!! note
+
+ The `path` type is special in that it will match any path, including `/` characters.
+ This is useful for creating routes that match a path prefix.
+
+So in practice these each might look like:
+
+```
+/my/route/{param}
+/my/route/{param:int}
+/my/route/{param:float}
+/my/route/{param:uuid}
+/my/route/{param:path}
+```
+
+Any route parameters collected from the current location then be accessed using the
+[`use_params`](#using-parameters) hook.
+
+!!! note
+
+ It's worth pointing out that, while you can use route parameters to capture values
+ from queryies (i.e. `?foo=bar`), this is not recommended. Instead, you should use
+ the [use_query][reactpy_router.use_query] hook to access query parameters.
+
+### Route Links
+
+Links between routes should be created using the [link][reactpy_router.link] component.
+This will allow ReactPy to handle the transition between routes more quickly by avoiding
+the cost of a full page load.
+
+```python
+from reactpy import component, html, run
+from reactpy_router import link, route, simple, use_location
+
+@component
+def root():
+ location = use_location()
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("/about", html.h1("About Page 📖")),
+ link("/about", html.button("About")),
+ )
+```
+
+## Hooks
+
+`reactpy-router` provides a number of hooks for working with the routes:
+
+- [`use_query`](#using-queries) - for accessing query parameters
+- [`use_params`](#using-parameters) - for accessing route parameters
+
+If you're not familiar with hooks, you should
+[read the docs](https://reactpy.dev/docs/guides/adding-interactivity/components-with-state/index.html#your-first-hook).
+
+### Using Queries
+
+The [use_query][reactpy_router.use_query] hook can be used to access query parameters
+from the current location. It returns a dictionary of query parameters, where each value
+is a list of strings.
+
+```python
+from reactpy import component, html, run
+from reactpy_router import link, route, simple, use_query
+
+@component
+def root():
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("/search", search()),
+ link("Search", to="/search?q=reactpy"),
+ )
+
+@component
+def search():
+ query = use_query()
+ return html.h1(f"Search Results for {query['q'][0]} 🔍")
+```
+
+### Using Parameters
+
+The [use_params][reactpy_router.use_params] hook can be used to access route parameters
+from the current location. It returns a dictionary of route parameters, where each value
+is mapped to a value that matches the type specified in the route path.
+
+```python
+from reactpy import component, html, run
+from reactpy_router import link, route, simple, use_params
+
+@component
+def root():
+ return simple.router(
+ route("/", html.h1("Home Page 🏠")),
+ route("/user/{id:int}", user()),
+ link("User 123", to="/user/123"),
+ )
+
+@component
+def user():
+ params = use_params()
+ return html.h1(f"User {params['id']} 👤")
+```
diff --git a/noxfile.py b/noxfile.py
index ee5f848..2103db6 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -13,6 +13,18 @@ def format(session: Session) -> None:
session.run("isort", ".")
+@session
+def docs(session: Session) -> None:
+ setup_docs(session)
+ session.run("mkdocs", "serve")
+
+
+@session
+def docs_build(session: Session) -> None:
+ setup_docs(session)
+ session.run("mkdocs", "build")
+
+
@session
def test(session: Session) -> None:
session.notify("test_style")
@@ -52,5 +64,11 @@ def test_suite(session: Session) -> None:
session.run("pytest", "tests", *posargs)
+def setup_docs(session: Session) -> None:
+ install_requirements(session, "build-docs")
+ session.install("-e", ".")
+ session.chdir("docs")
+
+
def install_requirements(session: Session, name: str) -> None:
session.install("-r", str(REQUIREMENTS_DIR / f"{name}.txt"))
diff --git a/reactpy_router/__init__.py b/reactpy_router/__init__.py
index 8d0c697..0fa3ea1 100644
--- a/reactpy_router/__init__.py
+++ b/reactpy_router/__init__.py
@@ -1,5 +1,5 @@
# the version is statically loaded by setup.py
-__version__ = "0.0.1"
+__version__ = "0.1.0"
from . import simple
from .core import create_router, link, route, router_component, use_params, use_query
diff --git a/reactpy_router/core.py b/reactpy_router/core.py
index df0105e..15d5b72 100644
--- a/reactpy_router/core.py
+++ b/reactpy_router/core.py
@@ -1,3 +1,5 @@
+"""Core functionality for the reactpy-router package."""
+
from __future__ import annotations
from dataclasses import dataclass, replace
@@ -25,6 +27,7 @@
def route(path: str, element: Any | None, *routes: Route) -> Route:
+ """Create a route with the given path, element, and child routes"""
return Route(path, element, routes)
@@ -42,6 +45,8 @@ def router_component(
*routes: R,
compiler: RouteCompiler[R],
) -> ComponentType | None:
+ """A component that renders the first matching route using the given compiler"""
+
old_conn = use_connection()
location, set_location = use_state(old_conn.location)
@@ -64,6 +69,7 @@ def router_component(
@component
def link(*children: VdomChild, to: str, **attributes: Any) -> VdomDict:
+ """A component that renders a link to the given path"""
set_location = _use_route_state().set_location
attrs = {
**attributes,
diff --git a/reactpy_router/py.typed b/reactpy_router/py.typed
new file mode 100644
index 0000000..7632ecf
--- /dev/null
+++ b/reactpy_router/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561
diff --git a/reactpy_router/simple.py b/reactpy_router/simple.py
index 3c6ea9b..566332e 100644
--- a/reactpy_router/simple.py
+++ b/reactpy_router/simple.py
@@ -1,3 +1,5 @@
+"""A simple router implementation for ReactPy"""
+
from __future__ import annotations
import re
@@ -15,9 +17,12 @@
ConverterMapping: TypeAlias = "dict[str, ConversionFunc]"
PARAM_REGEX = re.compile(r"{(?P\w+)(?P:\w+)?}")
+"""Regex for matching path params"""
class SimpleResolver:
+ """A simple route resolver that uses regex to match paths"""
+
def __init__(self, route: Route) -> None:
self.element = route.element
self.pattern, self.converters = parse_path(route.path)
@@ -34,6 +39,10 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
+ """Parse a path into a regex pattern and a mapping of converters"""
+ if path == "*":
+ return re.compile(".*"), {}
+
pattern = "^"
last_match_end = 0
converters: ConverterMapping = {}
@@ -53,8 +62,12 @@ def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
class ConversionInfo(TypedDict):
+ """Information about a conversion type"""
+
regex: str
+ """The regex to match the conversion type"""
func: ConversionFunc
+ """The function to convert the matched string to the expected type"""
CONVERSION_TYPES: dict[str, ConversionInfo] = {
@@ -79,6 +92,8 @@ class ConversionInfo(TypedDict):
"func": str,
},
}
+"""The supported conversion types"""
router = create_router(SimpleResolver)
+"""The simple router"""
diff --git a/reactpy_router/types.py b/reactpy_router/types.py
index 2a600ba..a91787e 100644
--- a/reactpy_router/types.py
+++ b/reactpy_router/types.py
@@ -1,3 +1,5 @@
+"""Types for reactpy_router"""
+
from __future__ import annotations
from dataclasses import dataclass, field
@@ -10,9 +12,16 @@
@dataclass(frozen=True)
class Route:
+ """A route that can be matched against a path"""
+
path: str
+ """The path to match against"""
+
element: Any = field(hash=False)
+ """The element to render if the path matches"""
+
routes: Sequence[Self]
+ """Child routes"""
def __hash__(self) -> int:
el = self.element
@@ -24,13 +33,17 @@ def __hash__(self) -> int:
class Router(Protocol[R]):
+ """Return a component that renders the first matching route"""
+
def __call__(self, *routes: R) -> ComponentType:
- """Return a component that renders the first matching route"""
+ ...
class RouteCompiler(Protocol[R]):
+ """Compile a route into a resolver that can be matched against a path"""
+
def __call__(self, route: R) -> RouteResolver:
- """Compile a route into a resolver that can be matched against a path"""
+ ...
class RouteResolver(Protocol):
diff --git a/requirements.txt b/requirements.txt
index 55f870e..5893bf2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+-r requirements/build-docs.txt
-r requirements/check-style.txt
-r requirements/check-types.txt
-r requirements/nox-deps.txt
diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt
new file mode 100644
index 0000000..3f19165
--- /dev/null
+++ b/requirements/build-docs.txt
@@ -0,0 +1,5 @@
+mkdocs
+mkdocs-material
+mkdocs-gen-files
+mkdocs-literate-nav
+mkdocstrings[python]
diff --git a/tests/test_simple.py b/tests/test_simple.py
index e7de017..a46cffe 100644
--- a/tests/test_simple.py
+++ b/tests/test_simple.py
@@ -48,3 +48,7 @@ def test_parse_path_re_escape():
re.compile(r"^/a/(?P\d+)/c\.d$"),
{"b": int},
)
+
+
+def test_match_any_path():
+ assert parse_path("*") == (re.compile(".*"), {})