diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8987d99..90e73f6 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,6 +1,3 @@ -# This workflow 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: Upload Python Package on: diff --git a/Makefile b/Makefile index 09d675c..66c5e03 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,13 @@ test: test-cov: ${PYTEST} --cov sanic_routing +.PHONY: fix +fix: + ruff check sanic_routing --fix + +.PHONY: format +format: + ruff format sanic_routing + .PHONY: pretty -pretty: - black --line-length 79 sanic_routing tests - isort --line-length 79 sanic_routing tests --profile=black +pretty: fix format diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4eedddb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[tool.ruff] +target-version = "py38" +line-length = 79 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "W", # pycodestyle warnings +] +ignore = [ + "E203", +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.isort] +known-first-party = ["sanic_routing"] +known-third-party = ["pytest"] +lines-after-imports = 2 +lines-between-types = 1 diff --git a/sanic_routing/__init__.py b/sanic_routing/__init__.py index 02da2aa..5a49e95 100644 --- a/sanic_routing/__init__.py +++ b/sanic_routing/__init__.py @@ -2,5 +2,6 @@ from .route import Route from .router import BaseRouter + __version__ = "23.6.0" __all__ = ("BaseRouter", "Route", "RouteGroup") diff --git a/sanic_routing/exceptions.py b/sanic_routing/exceptions.py index 9b9b1d3..d739e90 100644 --- a/sanic_routing/exceptions.py +++ b/sanic_routing/exceptions.py @@ -25,10 +25,12 @@ def __init__( message: str = "Method does not exist", method: Optional[str] = None, allowed_methods: Optional[Set[str]] = None, + path: Optional[str] = None, ): super().__init__(message) self.method = method self.allowed_methods = allowed_methods + self.path = path class FinalizationError(BaseException): diff --git a/sanic_routing/group.py b/sanic_routing/group.py index 4368847..bbcdfbf 100644 --- a/sanic_routing/group.py +++ b/sanic_routing/group.py @@ -104,6 +104,16 @@ def finalize(self): } ) + def prioritize_routes(self) -> None: + """ + Sorts the routes in the group by priority + """ + self._routes = tuple( + sorted( + self._routes, key=lambda route: route.priority, reverse=True + ) + ) + def reset(self): self.methods_index = dict(self.methods_index) @@ -163,6 +173,9 @@ def handler2(...): ) else: _routes.append(other_route) + _routes.sort( + key=lambda route: route.priority, reverse=True + ) self._routes = tuple(_routes) @property diff --git a/sanic_routing/patterns.py b/sanic_routing/patterns.py index 8bb777f..d00f92f 100644 --- a/sanic_routing/patterns.py +++ b/sanic_routing/patterns.py @@ -1,6 +1,7 @@ import re import typing as t import uuid + from datetime import date, datetime from types import SimpleNamespace from typing import Any, Callable, Dict, Pattern, Tuple, Type diff --git a/sanic_routing/route.py b/sanic_routing/route.py index a5088d2..eee17d1 100644 --- a/sanic_routing/route.py +++ b/sanic_routing/route.py @@ -1,5 +1,6 @@ import re import typing as t + from types import SimpleNamespace from warnings import warn @@ -28,6 +29,7 @@ class Route: "parts", "path", "pattern", + "priority", "regex", "requirements", "router", @@ -80,12 +82,15 @@ def __init__( static: bool = False, regex: bool = False, overloaded: bool = False, + *, + priority: int = 0, ): self.router = router self.name = name self.handler = handler # type: ignore self.methods = frozenset(methods) self.requirements = Requirements(requirements or {}) + self.priority = priority self.ctx = SimpleNamespace() self.extra = SimpleNamespace() @@ -326,15 +331,16 @@ def parse_parameter_string(self, parameter_string: str): """Parse a parameter string into its constituent name, type, and pattern - For example:: + For example: - parse_parameter_string('')` -> - ('param_one', '[A-z]', , '[A-z]') + ```text + parse_parameter_string('')` -> ('param_one', '[A-z]', , '[A-z]') + ``` :param parameter_string: String to parse :return: tuple containing (parameter_name, parameter_type, parameter_pattern) - """ + """ # noqa: E501 # We could receive NAME or NAME:PATTERN parameter_string = parameter_string.strip("<>") name = parameter_string diff --git a/sanic_routing/router.py b/sanic_routing/router.py index 0007d4f..2e9d6f9 100644 --- a/sanic_routing/router.py +++ b/sanic_routing/router.py @@ -1,6 +1,7 @@ import ast import sys import typing as t + from abc import ABC, abstractmethod from types import SimpleNamespace from warnings import warn @@ -21,6 +22,7 @@ from .tree import Node, Tree from .utils import parts_to_path, path_to_parts + # The below functions might be called by the compiled source code, and # therefore should be made available here by import import re # noqa isort:skip @@ -96,7 +98,7 @@ def resolve( orig=path, extra=extra, ) - raise self.exception(str(e), path=path) + raise e.__class__(str(e), path=path) if isinstance(route, RouteGroup): try: @@ -157,6 +159,8 @@ def add( unquote: bool = False, # noqa overwrite: bool = False, append: bool = False, + *, + priority: int = 0, ) -> Route: # Can add a route with overwrite, or append, not both. # - overwrite: if matching path exists, replace it @@ -166,6 +170,10 @@ def add( "Cannot add a route with both overwrite and append equal " "to True" ) + if priority and not append: + raise FinalizationError( + "Cannot add a route with priority if append is False" + ) if not methods: methods = [self.DEFAULT_METHOD] @@ -223,6 +231,7 @@ def add( unquote=unquote, static=static, regex=regex, + priority=priority, ) group = self.group_class(route) @@ -327,6 +336,7 @@ def finalize(self, do_compile: bool = True, do_optimize: bool = False): group.finalize() for route in group.routes: route.finalize() + group.prioritize_routes() # Evaluates all of the paths and arranges them into a hierarchichal # tree of nodes diff --git a/sanic_routing/tree.py b/sanic_routing/tree.py index 311e3c1..f912e2a 100644 --- a/sanic_routing/tree.py +++ b/sanic_routing/tree.py @@ -1,10 +1,12 @@ import typing as t + from logging import getLogger from .group import RouteGroup from .line import Line from .patterns import REGEX_PARAM_NAME, REGEX_PARAM_NAME_EXT, alpha, ext, slug + logger = getLogger("sanic.root") diff --git a/sanic_routing/utils.py b/sanic_routing/utils.py index 7624b9b..b37d306 100644 --- a/sanic_routing/utils.py +++ b/sanic_routing/utils.py @@ -1,4 +1,5 @@ import re + from urllib.parse import quote, unquote from sanic_routing.exceptions import InvalidUsage diff --git a/tox.ini b/tox.ini index 091ac40..17d3e88 100644 --- a/tox.ini +++ b/tox.ini @@ -16,15 +16,12 @@ commands = [testenv:lint] deps = - flake8 - black - isort + ruff mypy commands = - flake8 sanic_routing - black --line-length 79 --check sanic_routing - isort --line-length 79 --check sanic_routing --profile=black + ruff check sanic_routing + ruff format sanic_routing --check mypy sanic_routing [pytest]