Skip to content

Commit

Permalink
fix: typing of datastructures.URL (#2723)
Browse files Browse the repository at this point in the history
This PR fixes an issue where mypy would infer the type of `datastructures.URL` to be `Any`. This was caused by the use of the `@lrucache` decorator on `URL.__new__()`. We had ignored the error, however mypy would report:

> litestar/datastructures/url.py:85: error: Unsupported decorated constructor type  [misc]

The fix adds a new `@classmethod`, `URL._new()` which is cached and called from `URL.__new__()`.
  • Loading branch information
peterschutt authored Nov 20, 2023
1 parent 9b33f1d commit a2e5b78
Showing 1 changed file with 17 additions and 12 deletions.
29 changes: 17 additions & 12 deletions litestar/datastructures/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
from litestar.datastructures import MultiDict
from litestar.types import Empty

__all__ = ("Address", "URL")


if TYPE_CHECKING:
from typing_extensions import Self

from litestar.types import EmptyType, Scope

__all__ = ("Address", "URL")

_DEFAULT_SCHEME_PORTS = {"http": 80, "https": 443, "ftp": 21, "ws": 80, "wss": 443}


Expand Down Expand Up @@ -81,13 +82,17 @@ class URL:
hostname: str | None
"""Hostname if specified."""

@lru_cache # type: ignore[misc] # noqa: B019
def __new__(cls, url: str | SplitResult) -> URL:
"""Create a new instance.
Args:
url: url string or split result to represent.
"""
return cls._new(url=url)

@classmethod
@lru_cache
def _new(cls, url: str | SplitResult) -> URL:
instance = super().__new__(cls)
instance._parsed_url = None

Expand Down Expand Up @@ -135,7 +140,7 @@ def from_components(
path: str = "",
fragment: str = "",
query: str = "",
) -> URL:
) -> Self:
"""Create a new URL from components.
Args:
Expand All @@ -148,7 +153,7 @@ def from_components(
Returns:
A new URL with the given components
"""
return cls( # type: ignore[no-any-return]
return cls(
SplitResult(
scheme=scheme,
netloc=netloc,
Expand All @@ -159,7 +164,7 @@ def from_components(
)

@classmethod
def from_scope(cls, scope: Scope) -> URL:
def from_scope(cls, scope: Scope) -> Self:
"""Construct a URL from a :class:`Scope <.types.Scope>`
Args:
Expand Down Expand Up @@ -202,7 +207,7 @@ def with_replacements(
path: str = "",
query: str | MultiDict | None | EmptyType = Empty,
fragment: str = "",
) -> URL:
) -> Self:
"""Create a new URL, replacing the given components.
Args:
Expand All @@ -217,13 +222,13 @@ def with_replacements(
"""
if isinstance(query, MultiDict):
query = urlencode(query=query)
query = (query if query is not Empty else self.query) or ""
query_str = cast("str", (query if query is not Empty else self.query) or "")

return URL.from_components( # type: ignore[no-any-return]
return type(self).from_components(
scheme=scheme or self.scheme,
netloc=netloc or self.netloc,
path=path or self.path,
query=query,
query=query_str,
fragment=fragment or self.fragment,
)

Expand All @@ -250,7 +255,7 @@ def __str__(self) -> str:
def __eq__(self, other: Any) -> bool:
if isinstance(other, (str, URL)):
return str(self) == str(other)
return NotImplemented # type: ignore[unreachable] # pragma: no cover
return NotImplemented # pragma: no cover

def __repr__(self) -> str:
return f"{type(self).__name__}({self._url!r})"

0 comments on commit a2e5b78

Please sign in to comment.