Skip to content

Commit

Permalink
Started annotating IRIReference and URIReference.
Browse files Browse the repository at this point in the history
- Substituted namedtuple inheritance with common base `typing.NamedTuple` subclass in misc.py, since these classes share almost the exact same interface.
- Added a _typing_compat.py module to be able to import typing.Self, or a placeholder for it, in multiple other modules without bloating their code.
- Added basic method annotations to the two reference classes.
- Not annotations-related:
 - Move the __hash__ implementation over to IRIReference from URIMixin to be congruent with URIReference.
 - Made the __eq__ implementations more similar to avoid different behavior in cases of inheritance (rare as that might be).
 - Added overloads to `normalizers.normalize_query` and `normalizers.normalize_fragment` to clearly indicate that None will get passed through. This behavior is relied upon by the library currently.

- Note: The runtime-related changes can be reverted and reattempted later if need be. Still passing all the tests currently.
  • Loading branch information
Sachaa-Thanasius committed Jun 15, 2024
1 parent dd96a34 commit d99f274
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 34 deletions.
2 changes: 0 additions & 2 deletions src/rfc3986/_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
class URIMixin:
"""Mixin with all shared methods for URIs and IRIs."""

__hash__ = tuple.__hash__

def authority_info(self):
"""Return a dictionary with the ``userinfo``, ``host``, and ``port``.
Expand Down
19 changes: 19 additions & 0 deletions src/rfc3986/_typing_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import sys
import typing as t

__all__ = ("Self",)

if sys.version_info >= (3, 11):
from typing import Self
elif t.TYPE_CHECKING:
from typing_extensions import Self
else:

class _PlaceholderMeta(type):
# This is meant to make it easier to debug the presence of placeholder
# classes.
def __repr__(self):
return f"placeholder for typing.{self.__name__}"

class Self(metaclass=_PlaceholderMeta):
"""Placeholder for "typing.Self"."""
6 changes: 3 additions & 3 deletions src/rfc3986/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(
scheme: t.Optional[str] = None,
userinfo: t.Optional[str] = None,
host: t.Optional[str] = None,
port: t.Optional[str] = None,
port: t.Optional[t.Union[int, str]] = None,
path: t.Optional[str] = None,
query: t.Optional[str] = None,
fragment: t.Optional[str] = None,
Expand All @@ -60,7 +60,7 @@ def __init__(
(optional)
:param str host:
(optional)
:param int port:
:param int | str port:
(optional)
:param str path:
(optional)
Expand All @@ -72,7 +72,7 @@ def __init__(
self.scheme = scheme
self.userinfo = userinfo
self.host = host
self.port = port
self.port = str(port) if port is not None else port
self.path = path
self.query = query
self.fragment = fragment
Expand Down
39 changes: 26 additions & 13 deletions src/rfc3986/iri.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import typing as t
from collections import namedtuple

from . import compat
from . import exceptions
from . import misc
from . import normalizers
from . import uri
from ._typing_compat import Self


try:
Expand All @@ -29,9 +29,7 @@
idna = None


class IRIReference(
namedtuple("IRIReference", misc.URI_COMPONENTS), uri.URIMixin
):
class IRIReference(misc.URIReferenceBase, uri.URIMixin):
"""Immutable object representing a parsed IRI Reference.
Can be encoded into an URIReference object via the procedure
Expand All @@ -42,10 +40,16 @@ class IRIReference(
the future. Check for changes to the interface when upgrading.
"""

slots = ()
encoding: str

def __new__(
cls, scheme, authority, path, query, fragment, encoding="utf-8"
cls,
scheme: t.Optional[str],
authority: t.Optional[str],
path: t.Optional[str],
query: t.Optional[str],
fragment: t.Optional[str],
encoding: str = "utf-8",
):
"""Create a new IRIReference."""
ref = super().__new__(
Expand All @@ -59,14 +63,16 @@ def __new__(
ref.encoding = encoding
return ref

def __eq__(self, other):
__hash__ = tuple.__hash__

def __eq__(self, other: object):
"""Compare this reference to another."""
other_ref = other
if isinstance(other, tuple):
other_ref = self.__class__(*other)
other_ref = type(self)(*other)
elif not isinstance(other, IRIReference):
try:
other_ref = self.__class__.from_string(other)
other_ref = self.from_string(other)
except TypeError:
raise TypeError(
"Unable to compare {}() to {}()".format(
Expand All @@ -77,15 +83,15 @@ def __eq__(self, other):
# See http://tools.ietf.org/html/rfc3986#section-6.2
return tuple(self) == tuple(other_ref)

def _match_subauthority(self):
def _match_subauthority(self) -> t.Optional[t.Match[str]]:
return misc.ISUBAUTHORITY_MATCHER.match(self.authority)

@classmethod
def from_string(
cls,
iri_string: t.Union[str, bytes, bytearray],
encoding: str = "utf-8",
):
) -> Self:
"""Parse a IRI reference from the given unicode IRI string.
:param str iri_string: Unicode IRI to be parsed into a reference.
Expand All @@ -104,7 +110,12 @@ def from_string(
encoding,
)

def encode(self, idna_encoder=None): # noqa: C901
def encode( # noqa: C901
self,
idna_encoder: t.Optional[ # pyright: ignore[reportRedeclaration]
t.Callable[[str], t.Union[str, bytes]]
] = None,
) -> "uri.URIReference":
"""Encode an IRIReference into a URIReference instance.
If the ``idna`` module is installed or the ``rfc3986[idna]``
Expand All @@ -127,7 +138,9 @@ def encode(self, idna_encoder=None): # noqa: C901
"and the IRI hostname requires encoding"
)

def idna_encoder(name):
def idna_encoder(name: str) -> t.Union[str, bytes]:
assert idna # Known to not be None at this point.

if any(ord(c) > 128 for c in name):
try:
return idna.encode(
Expand Down
13 changes: 10 additions & 3 deletions src/rfc3986/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@
# Break an import loop.
from . import uri

# These are enumerated for the named tuple used as a superclass of
# URIReference
URI_COMPONENTS = ["scheme", "authority", "path", "query", "fragment"]

class URIReferenceBase(t.NamedTuple):
"""The namedtuple used as a superclass of URIReference and IRIReference."""

scheme: t.Optional[str]
authority: t.Optional[str]
path: t.Optional[str]
query: t.Optional[str]
fragment: t.Optional[str]


important_characters = {
"generic_delimiters": abnf_regexp.GENERIC_DELIMITERS,
Expand Down
24 changes: 22 additions & 2 deletions src/rfc3986/normalizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,34 @@ def normalize_path(path: str) -> str:
return remove_dot_segments(path)


def normalize_query(query: str) -> str:
@t.overload
def normalize_query(query: str) -> str: # noqa: D103
...


@t.overload
def normalize_query(query: None) -> None: # noqa: D103
...


def normalize_query(query: t.Optional[str]) -> t.Optional[str]:
"""Normalize the query string."""
if not query:
return query
return normalize_percent_characters(query)


def normalize_fragment(fragment: str) -> str:
@t.overload
def normalize_fragment(fragment: str) -> str: # noqa: D103
...


@t.overload
def normalize_fragment(fragment: None) -> None: # noqa: D103
...


def normalize_fragment(fragment: t.Optional[str]) -> t.Optional[str]:
"""Normalize the fragment string."""
if not fragment:
return fragment
Expand Down
28 changes: 17 additions & 11 deletions src/rfc3986/uri.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import typing as t
from collections import namedtuple

from . import compat
from . import misc
from . import normalizers
from ._mixin import URIMixin
from ._typing_compat import Self


class URIReference(namedtuple("URIReference", misc.URI_COMPONENTS), URIMixin):
class URIReference(misc.URIReferenceBase, URIMixin):
"""Immutable object representing a parsed URI Reference.
.. note::
Expand Down Expand Up @@ -80,10 +80,16 @@ class URIReference(namedtuple("URIReference", misc.URI_COMPONENTS), URIMixin):
The port parsed from the authority.
"""

slots = ()
encoding: str

def __new__(
cls, scheme, authority, path, query, fragment, encoding="utf-8"
cls,
scheme: t.Optional[str],
authority: t.Optional[str],
path: t.Optional[str],
query: t.Optional[str],
fragment: t.Optional[str],
encoding: str = "utf-8",
):
"""Create a new URIReference."""
ref = super().__new__(
Expand All @@ -99,26 +105,26 @@ def __new__(

__hash__ = tuple.__hash__

def __eq__(self, other):
def __eq__(self, other: object):
"""Compare this reference to another."""
other_ref = other
if isinstance(other, tuple):
other_ref = URIReference(*other)
other_ref = type(self)(*other)
elif not isinstance(other, URIReference):
try:
other_ref = URIReference.from_string(other)
other_ref = self.from_string(other)
except TypeError:
raise TypeError(
"Unable to compare URIReference() to {}()".format(
type(other).__name__
"Unable to compare {}() to {}()".format(
type(self).__name__, type(other).__name__
)
)

# See http://tools.ietf.org/html/rfc3986#section-6.2
naive_equality = tuple(self) == tuple(other_ref)
return naive_equality or self.normalized_equality(other_ref)

def normalize(self):
def normalize(self) -> "URIReference":
"""Normalize this reference as described in Section 6.2.2.
This is not an in-place normalization. Instead this creates a new
Expand All @@ -145,7 +151,7 @@ def from_string(
cls,
uri_string: t.Union[str, bytes, bytearray],
encoding: str = "utf-8",
):
) -> Self:
"""Parse a URI reference from the given unicode URI string.
:param str uri_string: Unicode URI to be parsed into a reference.
Expand Down

0 comments on commit d99f274

Please sign in to comment.