Source code for next.urls.parser

"""Map bracket segments in file-based URL paths to Django converters.

The `URLPatternParser` turns a filesystem-style logical URL trail into
a Django path pattern. Bracket syntax `[name]` maps to `<str:name>`,
`[int:id]` maps to `<int:id>`, and `[[args]]` maps to `<path:args>`.
"""

from __future__ import annotations

import re
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import TYPE_CHECKING, ClassVar
from uuid import UUID


if TYPE_CHECKING:
    from collections.abc import Callable


def _coerce_bool(text: str) -> bool:
    return text.lower() in ("1", "true", "yes")


_COERCERS: dict[type, Callable[[str], object]] = {
    str: str,
    int: int,
    bool: _coerce_bool,
    float: float,
    UUID: UUID,
    Decimal: Decimal,
    datetime: datetime.fromisoformat,
    date: date.fromisoformat,
}


def _coerce_url_value(value: object, hint: object) -> object:
    """Coerce `value` to `hint`, passing it through on failure or unsupported hint."""
    if not isinstance(hint, type):
        return value
    if isinstance(value, hint):
        return value
    coercer = _COERCERS.get(hint)
    if coercer is None:
        return value
    text = value if isinstance(value, str) else str(value)
    try:
        return coercer(text)
    except (ValueError, InvalidOperation):
        return value


[docs] class URLPatternParser: """Map bracket segments in a file-based path to Django path converters. The `url_path` string is the logical URL trail built from directory names. An empty string means the tree root. It is not a `pathlib.Path`. The on-disk file is the second value from the page-tree scanner. """ _param_pattern: ClassVar[re.Pattern[str]] = re.compile(r"\[([^\[\]]+)\]") _args_pattern: ClassVar[re.Pattern[str]] = re.compile(r"\[\[([^\[\]]+)\]\]")
[docs] def parse_url_pattern(self, url_path: str) -> tuple[str, dict[str, str]]: """Return the Django path string and parameter names for `url_path`.""" django_pattern = url_path parameters: dict[str, str] = {} if args_match := self._args_pattern.search(django_pattern): args_name = args_match.group(1) django_args_name = args_name.replace("-", "_") django_pattern = self._args_pattern.sub( f"<path:{django_args_name}>", django_pattern, ) parameters[django_args_name] = django_args_name param_matches = self._param_pattern.findall(url_path) for param_str in param_matches: param_name, param_type = self._parse_param_name_and_type(param_str) django_param_name = param_name.replace("-", "_") django_pattern = django_pattern.replace( f"[{param_str}]", f"<{param_type}:{django_param_name}>", ) parameters[django_param_name] = django_param_name if django_pattern and not django_pattern.endswith("/"): django_pattern = f"{django_pattern}/" return django_pattern, parameters
def _parse_param_name_and_type(self, param_str: str) -> tuple[str, str]: """Split bracket text into a name and converter label (default `str`).""" if ":" in param_str: type_name, param_name = param_str.split(":", 1) return param_name.strip(), type_name.strip() return param_str.strip(), "str" _name_sep_pattern: ClassVar[re.Pattern[str]] = re.compile(r"[/\[\]:\-_]+")
[docs] def prepare_url_name(self, url_path: str) -> str: """Python-safe name for `reverse` from a filesystem-style `url_path`.""" return self._name_sep_pattern.sub("_", url_path).strip("_")
default_url_parser: URLPatternParser = URLPatternParser() __all__ = ["URLPatternParser", "default_url_parser"]