Skip to content

add robust lint/testing + upgrade idom + more robust routing with Starlette #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 18, 2022
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
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: Bug Report
labels: bug
assignees: rmorshea

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Additional context**
Add any other context about the problem here.
14 changes: 14 additions & 0 deletions .github/ISSUE_TEMPLATE/doc_enhancement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: Doc enhancement
about: Documentation needs to be fixed or added
title: Doc Enhancement
labels: docs
assignees: rmorshea

---

**Describe what documentation needs to be fixed or added**
Is something missing, worded poorly, or flat out wrong? Tells us about it here.

**Additional context**
Add any other context about the problem here.
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: rmorshea

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
30 changes: 30 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Release

on:
release:
types:
- created

jobs:
publish-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py bdist_wheel
twine upload dist/*
33 changes: 33 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Test

on: [push]

jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Latest Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install Python Dependencies
run: pip install -r requirements/nox-deps.txt
- name: Run Tests
run: nox -s test

environments:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Python Dependencies
run: pip install -r requirements/nox-deps.txt
- name: Run Tests
run: nox -s test -- --no-cov
20 changes: 20 additions & 0 deletions idom_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,22 @@
# the version is statically loaded by setup.py
__version__ = "0.0.1"

from .router import (
Route,
RoutesConstructor,
configure,
link,
use_location,
use_params,
use_query,
)

__all__ = [
"configure",
"link",
"Route",
"RoutesConstructor",
"use_location",
"use_params",
"use_query",
]
153 changes: 112 additions & 41 deletions idom_router/router.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,138 @@
from __future__ import annotations
from dataclasses import dataclass

from fnmatch import translate as fnmatch_translate
from pathlib import Path
import re
from typing import Any, Iterator, Protocol, Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Iterator, Sequence
from urllib.parse import parse_qs

from idom import create_context, component, use_context, use_state
from idom.web.module import export, module_from_file
from idom.core.vdom import coalesce_attributes_and_children, VdomAttributesAndChildren
from idom import component, create_context, use_context, use_memo, use_state
from idom.core.types import VdomAttributesAndChildren, VdomDict
from idom.core.vdom import coalesce_attributes_and_children
from idom.types import BackendImplementation, ComponentType, Context, Location
from idom.web.module import export, module_from_file
from starlette.routing import compile_path

try:
from typing import Protocol
except ImportError: # pragma: no cover
from typing_extensions import Protocol # type: ignore


class Router(Protocol):
class RoutesConstructor(Protocol):
def __call__(self, *routes: Route) -> ComponentType:
...


def bind(backend: BackendImplementation) -> Router:
def configure(
implementation: BackendImplementation[Any] | Callable[[], Location]
) -> RoutesConstructor:
if isinstance(implementation, BackendImplementation):
use_location = implementation.use_location
elif callable(implementation):
use_location = implementation
else:
raise TypeError(
"Expected a 'BackendImplementation' or "
f"'use_location' hook, not {implementation}"
)

@component
def Router(*routes: Route):
initial_location = backend.use_location()
def routes(*routes: Route) -> ComponentType | None:
initial_location = use_location()
location, set_location = use_state(initial_location)
for p, r in _compile_routes(routes):
if p.match(location.pathname):
compiled_routes = use_memo(
lambda: _iter_compile_routes(routes), dependencies=routes
)
for r in compiled_routes:
match = r.pattern.match(location.pathname)
if match:
return _LocationStateContext(
r.element,
value=(location, set_location),
key=r.path,
value=_LocationState(
location,
set_location,
{k: r.converters[k](v) for k, v in match.groupdict().items()},
),
key=r.pattern.pattern,
)
return None

return Router


def use_location() -> str:
return _use_location_state()[0]
return routes


@dataclass
class Route:
path: str | re.Pattern
path: str
element: Any
routes: Sequence[Route]

def __init__(self, path: str, element: Any | None, *routes: Route) -> None:
self.path = path
self.element = element
self.routes = routes


@component
def Link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> None:
def link(*attributes_or_children: VdomAttributesAndChildren, to: str) -> VdomDict:
attributes, children = coalesce_attributes_and_children(attributes_or_children)
set_location = _use_location_state()[1]
return _Link(
{
**attributes,
"to": to,
"onClick": lambda event: set_location(Location(**event)),
},
*children,
set_location = _use_location_state().set_location
attrs = {
**attributes,
"to": to,
"onClick": lambda event: set_location(Location(**event)),
}
return _Link(attrs, *children)


def use_location() -> Location:
"""Get the current route location"""
return _use_location_state().location


def use_params() -> dict[str, Any]:
"""Get parameters from the currently matching route pattern"""
return _use_location_state().params


def use_query(
keep_blank_values: bool = False,
strict_parsing: bool = False,
errors: str = "replace",
max_num_fields: int | None = None,
separator: str = "&",
) -> dict[str, list[str]]:
"""See :func:`urllib.parse.parse_qs` for parameter info."""
return parse_qs(
use_location().search[1:],
keep_blank_values=keep_blank_values,
strict_parsing=strict_parsing,
errors=errors,
max_num_fields=max_num_fields,
separator=separator,
)


def _compile_routes(routes: Sequence[Route]) -> Iterator[tuple[re.Pattern, Route]]:
def _iter_compile_routes(routes: Sequence[Route]) -> Iterator[_CompiledRoute]:
for path, element in _iter_routes(routes):
pattern, _, converters = compile_path(path)
yield _CompiledRoute(
pattern, {k: v.convert for k, v in converters.items()}, element
)


def _iter_routes(routes: Sequence[Route]) -> Iterator[tuple[str, Any]]:
for r in routes:
if isinstance(r.path, re.Pattern):
yield r.path, r
continue
if not r.path.startswith("/"):
raise ValueError("Path pattern must begin with '/'")
pattern = re.compile(fnmatch_translate(r.path))
yield pattern, r
for path, element in _iter_routes(r.routes):
yield r.path + path, element
yield r.path, r.element


@dataclass
class _CompiledRoute:
pattern: re.Pattern[str]
converters: dict[str, Callable[[Any], Any]]
element: Any


def _use_location_state() -> _LocationState:
Expand All @@ -75,9 +141,14 @@ def _use_location_state() -> _LocationState:
return location_state


_LocationSetter = Callable[[str], None]
_LocationState = tuple[Location, _LocationSetter]
_LocationStateContext: type[Context[_LocationState | None]] = create_context(None)
@dataclass
class _LocationState:
location: Location
set_location: Callable[[Location], None]
params: dict[str, Any]


_LocationStateContext: Context[_LocationState | None] = create_context(None)

_Link = export(
module_from_file(
Expand Down
Loading