Skip to content

Commit d99f274

Browse files
Started annotating IRIReference and URIReference.
- 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.
1 parent dd96a34 commit d99f274

File tree

7 files changed

+97
-34
lines changed

7 files changed

+97
-34
lines changed

src/rfc3986/_mixin.py

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
class URIMixin:
1111
"""Mixin with all shared methods for URIs and IRIs."""
1212

13-
__hash__ = tuple.__hash__
14-
1513
def authority_info(self):
1614
"""Return a dictionary with the ``userinfo``, ``host``, and ``port``.
1715

src/rfc3986/_typing_compat.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import sys
2+
import typing as t
3+
4+
__all__ = ("Self",)
5+
6+
if sys.version_info >= (3, 11):
7+
from typing import Self
8+
elif t.TYPE_CHECKING:
9+
from typing_extensions import Self
10+
else:
11+
12+
class _PlaceholderMeta(type):
13+
# This is meant to make it easier to debug the presence of placeholder
14+
# classes.
15+
def __repr__(self):
16+
return f"placeholder for typing.{self.__name__}"
17+
18+
class Self(metaclass=_PlaceholderMeta):
19+
"""Placeholder for "typing.Self"."""

src/rfc3986/builder.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(
4747
scheme: t.Optional[str] = None,
4848
userinfo: t.Optional[str] = None,
4949
host: t.Optional[str] = None,
50-
port: t.Optional[str] = None,
50+
port: t.Optional[t.Union[int, str]] = None,
5151
path: t.Optional[str] = None,
5252
query: t.Optional[str] = None,
5353
fragment: t.Optional[str] = None,
@@ -60,7 +60,7 @@ def __init__(
6060
(optional)
6161
:param str host:
6262
(optional)
63-
:param int port:
63+
:param int | str port:
6464
(optional)
6565
:param str path:
6666
(optional)
@@ -72,7 +72,7 @@ def __init__(
7272
self.scheme = scheme
7373
self.userinfo = userinfo
7474
self.host = host
75-
self.port = port
75+
self.port = str(port) if port is not None else port
7676
self.path = path
7777
self.query = query
7878
self.fragment = fragment

src/rfc3986/iri.py

+26-13
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
import typing as t
17-
from collections import namedtuple
1817

1918
from . import compat
2019
from . import exceptions
2120
from . import misc
2221
from . import normalizers
2322
from . import uri
23+
from ._typing_compat import Self
2424

2525

2626
try:
@@ -29,9 +29,7 @@
2929
idna = None
3030

3131

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

45-
slots = ()
43+
encoding: str
4644

4745
def __new__(
48-
cls, scheme, authority, path, query, fragment, encoding="utf-8"
46+
cls,
47+
scheme: t.Optional[str],
48+
authority: t.Optional[str],
49+
path: t.Optional[str],
50+
query: t.Optional[str],
51+
fragment: t.Optional[str],
52+
encoding: str = "utf-8",
4953
):
5054
"""Create a new IRIReference."""
5155
ref = super().__new__(
@@ -59,14 +63,16 @@ def __new__(
5963
ref.encoding = encoding
6064
return ref
6165

62-
def __eq__(self, other):
66+
__hash__ = tuple.__hash__
67+
68+
def __eq__(self, other: object):
6369
"""Compare this reference to another."""
6470
other_ref = other
6571
if isinstance(other, tuple):
66-
other_ref = self.__class__(*other)
72+
other_ref = type(self)(*other)
6773
elif not isinstance(other, IRIReference):
6874
try:
69-
other_ref = self.__class__.from_string(other)
75+
other_ref = self.from_string(other)
7076
except TypeError:
7177
raise TypeError(
7278
"Unable to compare {}() to {}()".format(
@@ -77,15 +83,15 @@ def __eq__(self, other):
7783
# See http://tools.ietf.org/html/rfc3986#section-6.2
7884
return tuple(self) == tuple(other_ref)
7985

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

8389
@classmethod
8490
def from_string(
8591
cls,
8692
iri_string: t.Union[str, bytes, bytearray],
8793
encoding: str = "utf-8",
88-
):
94+
) -> Self:
8995
"""Parse a IRI reference from the given unicode IRI string.
9096
9197
:param str iri_string: Unicode IRI to be parsed into a reference.
@@ -104,7 +110,12 @@ def from_string(
104110
encoding,
105111
)
106112

107-
def encode(self, idna_encoder=None): # noqa: C901
113+
def encode( # noqa: C901
114+
self,
115+
idna_encoder: t.Optional[ # pyright: ignore[reportRedeclaration]
116+
t.Callable[[str], t.Union[str, bytes]]
117+
] = None,
118+
) -> "uri.URIReference":
108119
"""Encode an IRIReference into a URIReference instance.
109120
110121
If the ``idna`` module is installed or the ``rfc3986[idna]``
@@ -127,7 +138,9 @@ def encode(self, idna_encoder=None): # noqa: C901
127138
"and the IRI hostname requires encoding"
128139
)
129140

130-
def idna_encoder(name):
141+
def idna_encoder(name: str) -> t.Union[str, bytes]:
142+
assert idna # Known to not be None at this point.
143+
131144
if any(ord(c) > 128 for c in name):
132145
try:
133146
return idna.encode(

src/rfc3986/misc.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@
2626
# Break an import loop.
2727
from . import uri
2828

29-
# These are enumerated for the named tuple used as a superclass of
30-
# URIReference
31-
URI_COMPONENTS = ["scheme", "authority", "path", "query", "fragment"]
29+
30+
class URIReferenceBase(t.NamedTuple):
31+
"""The namedtuple used as a superclass of URIReference and IRIReference."""
32+
33+
scheme: t.Optional[str]
34+
authority: t.Optional[str]
35+
path: t.Optional[str]
36+
query: t.Optional[str]
37+
fragment: t.Optional[str]
38+
3239

3340
important_characters = {
3441
"generic_delimiters": abnf_regexp.GENERIC_DELIMITERS,

src/rfc3986/normalizers.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,34 @@ def normalize_path(path: str) -> str:
8282
return remove_dot_segments(path)
8383

8484

85-
def normalize_query(query: str) -> str:
85+
@t.overload
86+
def normalize_query(query: str) -> str: # noqa: D103
87+
...
88+
89+
90+
@t.overload
91+
def normalize_query(query: None) -> None: # noqa: D103
92+
...
93+
94+
95+
def normalize_query(query: t.Optional[str]) -> t.Optional[str]:
8696
"""Normalize the query string."""
8797
if not query:
8898
return query
8999
return normalize_percent_characters(query)
90100

91101

92-
def normalize_fragment(fragment: str) -> str:
102+
@t.overload
103+
def normalize_fragment(fragment: str) -> str: # noqa: D103
104+
...
105+
106+
107+
@t.overload
108+
def normalize_fragment(fragment: None) -> None: # noqa: D103
109+
...
110+
111+
112+
def normalize_fragment(fragment: t.Optional[str]) -> t.Optional[str]:
93113
"""Normalize the fragment string."""
94114
if not fragment:
95115
return fragment

src/rfc3986/uri.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
import typing as t
17-
from collections import namedtuple
1817

1918
from . import compat
2019
from . import misc
2120
from . import normalizers
2221
from ._mixin import URIMixin
22+
from ._typing_compat import Self
2323

2424

25-
class URIReference(namedtuple("URIReference", misc.URI_COMPONENTS), URIMixin):
25+
class URIReference(misc.URIReferenceBase, URIMixin):
2626
"""Immutable object representing a parsed URI Reference.
2727
2828
.. note::
@@ -80,10 +80,16 @@ class URIReference(namedtuple("URIReference", misc.URI_COMPONENTS), URIMixin):
8080
The port parsed from the authority.
8181
"""
8282

83-
slots = ()
83+
encoding: str
8484

8585
def __new__(
86-
cls, scheme, authority, path, query, fragment, encoding="utf-8"
86+
cls,
87+
scheme: t.Optional[str],
88+
authority: t.Optional[str],
89+
path: t.Optional[str],
90+
query: t.Optional[str],
91+
fragment: t.Optional[str],
92+
encoding: str = "utf-8",
8793
):
8894
"""Create a new URIReference."""
8995
ref = super().__new__(
@@ -99,26 +105,26 @@ def __new__(
99105

100106
__hash__ = tuple.__hash__
101107

102-
def __eq__(self, other):
108+
def __eq__(self, other: object):
103109
"""Compare this reference to another."""
104110
other_ref = other
105111
if isinstance(other, tuple):
106-
other_ref = URIReference(*other)
112+
other_ref = type(self)(*other)
107113
elif not isinstance(other, URIReference):
108114
try:
109-
other_ref = URIReference.from_string(other)
115+
other_ref = self.from_string(other)
110116
except TypeError:
111117
raise TypeError(
112-
"Unable to compare URIReference() to {}()".format(
113-
type(other).__name__
118+
"Unable to compare {}() to {}()".format(
119+
type(self).__name__, type(other).__name__
114120
)
115121
)
116122

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

121-
def normalize(self):
127+
def normalize(self) -> "URIReference":
122128
"""Normalize this reference as described in Section 6.2.2.
123129
124130
This is not an in-place normalization. Instead this creates a new
@@ -145,7 +151,7 @@ def from_string(
145151
cls,
146152
uri_string: t.Union[str, bytes, bytearray],
147153
encoding: str = "utf-8",
148-
):
154+
) -> Self:
149155
"""Parse a URI reference from the given unicode URI string.
150156
151157
:param str uri_string: Unicode URI to be parsed into a reference.

0 commit comments

Comments
 (0)