Skip to content

Commit 3c22353

Browse files
More annotations that I forgot to break up into multiple commits.
- validators.py - Finish annotating return types. - Change ensure_one_of takes a `Collection`, not a `Container`, since it needs to be iterable within `UnpermittedComponentError.__init__`. - Change `authority_is_valid` to permit None as an input; continuation of making sure is_valid allowing None propogates. Also, this behavior is depended on elsewhere in the library (just one spot, I think). - parseresult.py - Add variable annotations to `ParseResultMixin`, and make sure _generate_authority is allowed to return `None`. - Fix `ParseResultBytes.copy_with` not accepting an int for port. - Annotate return type for `authority_from`. - misc.py - Use common base for `URIReference` and `IRIReference` as annotation for `merge_path` and remove circular import. - exceptions.py - Annotate everything. - _mixin.py - Add variable annotations to `URIMixin`; they're under a TYPE_CHECKING block so that only the subclasse's annotations can be found in cases of introspection. Might be overkill. - Use `uri.URIReference` to annotate parameters for various functions. - TODO: Check if these are potentially too wide, since `IRIReference` also exists and inherits from `URIMixin`? - Use hacky "typing.cast within an elided if block" trick to improve typing within `URIMixin.resolve_with`.
1 parent 9862d65 commit 3c22353

File tree

5 files changed

+73
-36
lines changed

5 files changed

+73
-36
lines changed

src/rfc3986/_mixin.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from . import exceptions as exc
66
from . import misc
77
from . import normalizers
8+
from . import uri
89
from . import validators
910
from ._typing_compat import Self as _Self
1011

@@ -20,6 +21,14 @@ class _AuthorityInfo(t.TypedDict):
2021
class URIMixin:
2122
"""Mixin with all shared methods for URIs and IRIs."""
2223

24+
if t.TYPE_CHECKING:
25+
scheme: t.Optional[str]
26+
authority: t.Optional[str]
27+
path: t.Optional[str]
28+
query: t.Optional[str]
29+
fragment: t.Optional[str]
30+
encoding: str
31+
2332
def authority_info(self) -> _AuthorityInfo:
2433
"""Return a dictionary with the ``userinfo``, ``host``, and ``port``.
2534
@@ -251,7 +260,7 @@ def fragment_is_valid(self, require: bool = False) -> bool:
251260
)
252261
return validators.fragment_is_valid(self.fragment, require)
253262

254-
def normalized_equality(self, other_ref) -> bool:
263+
def normalized_equality(self, other_ref: "uri.URIReference") -> bool:
255264
"""Compare this URIReference to another URIReference.
256265
257266
:param URIReference other_ref: (required), The reference with which
@@ -261,7 +270,11 @@ def normalized_equality(self, other_ref) -> bool:
261270
"""
262271
return tuple(self.normalize()) == tuple(other_ref.normalize())
263272

264-
def resolve_with(self, base_uri, strict: bool = False) -> _Self:
273+
def resolve_with(
274+
self,
275+
base_uri: t.Union[str, "uri.URIReference"],
276+
strict: bool = False,
277+
) -> _Self:
265278
"""Use an absolute URI Reference to resolve this relative reference.
266279
267280
Assuming this is a relative reference that you would like to resolve,
@@ -280,6 +293,9 @@ def resolve_with(self, base_uri, strict: bool = False) -> _Self:
280293
if not isinstance(base_uri, URIMixin):
281294
base_uri = type(self).from_string(base_uri)
282295

296+
if t.TYPE_CHECKING:
297+
base_uri = t.cast(uri.URIReference, base_uri)
298+
283299
try:
284300
self._validator.validate(base_uri)
285301
except exc.ValidationError:
@@ -388,6 +404,6 @@ def copy_with(
388404
for key, value in list(attributes.items()):
389405
if value is misc.UseExisting:
390406
del attributes[key]
391-
uri = self._replace(**attributes)
407+
uri: "uri.URIReference" = self._replace(**attributes)
392408
uri.encoding = self.encoding
393409
return uri

src/rfc3986/exceptions.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Exceptions module for rfc3986."""
2+
import typing as t
3+
24
from . import compat
5+
from . import uri
36

47

58
class RFC3986Exception(Exception):
@@ -11,7 +14,7 @@ class RFC3986Exception(Exception):
1114
class InvalidAuthority(RFC3986Exception):
1215
"""Exception when the authority string is invalid."""
1316

14-
def __init__(self, authority):
17+
def __init__(self, authority: t.Union[str, bytes]) -> None:
1518
"""Initialize the exception with the invalid authority."""
1619
super().__init__(
1720
f"The authority ({compat.to_str(authority)}) is not valid."
@@ -21,15 +24,15 @@ def __init__(self, authority):
2124
class InvalidPort(RFC3986Exception):
2225
"""Exception when the port is invalid."""
2326

24-
def __init__(self, port):
27+
def __init__(self, port: str) -> None:
2528
"""Initialize the exception with the invalid port."""
2629
super().__init__(f'The port ("{port}") is not valid.')
2730

2831

2932
class ResolutionError(RFC3986Exception):
3033
"""Exception to indicate a failure to resolve a URI."""
3134

32-
def __init__(self, uri):
35+
def __init__(self, uri: "uri.URIReference") -> None:
3336
"""Initialize the error with the failed URI."""
3437
super().__init__(
3538
"{} does not meet the requirements for resolution.".format(
@@ -47,7 +50,7 @@ class ValidationError(RFC3986Exception):
4750
class MissingComponentError(ValidationError):
4851
"""Exception raised when a required component is missing."""
4952

50-
def __init__(self, uri, *component_names):
53+
def __init__(self, uri: "uri.URIReference", *component_names: str) -> None:
5154
"""Initialize the error with the missing component name."""
5255
verb = "was"
5356
if len(component_names) > 1:
@@ -66,7 +69,12 @@ def __init__(self, uri, *component_names):
6669
class UnpermittedComponentError(ValidationError):
6770
"""Exception raised when a component has an unpermitted value."""
6871

69-
def __init__(self, component_name, component_value, allowed_values):
72+
def __init__(
73+
self,
74+
component_name: str,
75+
component_value: t.Any,
76+
allowed_values: t.Collection[t.Any],
77+
) -> None:
7078
"""Initialize the error with the unpermitted component."""
7179
super().__init__(
7280
"{} was required to be one of {!r} but was {!r}".format(
@@ -86,7 +94,7 @@ def __init__(self, component_name, component_value, allowed_values):
8694
class PasswordForbidden(ValidationError):
8795
"""Exception raised when a URL has a password in the userinfo section."""
8896

89-
def __init__(self, uri):
97+
def __init__(self, uri: t.Union[str, "uri.URIReference"]) -> None:
9098
"""Initialize the error with the URI that failed validation."""
9199
unsplit = getattr(uri, "unsplit", lambda: uri)
92100
super().__init__(
@@ -100,7 +108,7 @@ def __init__(self, uri):
100108
class InvalidComponentsError(ValidationError):
101109
"""Exception raised when one or more components are invalid."""
102110

103-
def __init__(self, uri, *component_names):
111+
def __init__(self, uri: "uri.URIReference", *component_names: str) -> None:
104112
"""Initialize the error with the invalid component name(s)."""
105113
verb = "was"
106114
if len(component_names) > 1:

src/rfc3986/misc.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222

2323
from . import abnf_regexp
2424

25-
if t.TYPE_CHECKING:
26-
# Break an import loop.
27-
from . import uri
28-
2925

3026
class URIReferenceBase(t.NamedTuple):
3127
"""The namedtuple used as a superclass of URIReference and IRIReference."""
@@ -130,7 +126,7 @@ class URIReferenceBase(t.NamedTuple):
130126

131127

132128
# Path merger as defined in http://tools.ietf.org/html/rfc3986#section-5.2.3
133-
def merge_paths(base_uri: "uri.URIReference", relative_path: str) -> str:
129+
def merge_paths(base_uri: URIReferenceBase, relative_path: str) -> str:
134130
"""Merge a base URI's path with a relative URI's path."""
135131
if base_uri.path is None and base_uri.authority is not None:
136132
return "/" + relative_path

src/rfc3986/parseresult.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,21 @@
3636

3737

3838
class ParseResultMixin(t.Generic[t.AnyStr]):
39+
if t.TYPE_CHECKING:
40+
userinfo: t.Optional[t.AnyStr]
41+
host: t.Optional[t.AnyStr]
42+
port: t.Optional[int]
43+
query: t.Optional[t.AnyStr]
44+
encoding: str
45+
46+
@property
47+
def authority(self) -> t.Optional[t.AnyStr]:
48+
...
49+
3950
def _generate_authority(
4051
self,
4152
attributes: t.Dict[str, t.Optional[t.AnyStr]],
42-
) -> str:
53+
) -> t.Optional[str]:
4354
# I swear I did not align the comparisons below. That's just how they
4455
# happened to align based on pep8 and attribute lengths.
4556
userinfo, host, port = (
@@ -402,7 +413,7 @@ def copy_with(
402413
scheme: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
403414
userinfo: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
404415
host: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
405-
port: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
416+
port: t.Optional[t.Union[int, str, bytes]] = misc.UseExisting,
406417
path: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
407418
query: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
408419
fragment: t.Optional[t.Union[str, bytes]] = misc.UseExisting,
@@ -490,7 +501,10 @@ def split_authority(
490501
return userinfo, host, port
491502

492503

493-
def authority_from(reference: "uri.URIReference", strict: bool):
504+
def authority_from(
505+
reference: "uri.URIReference",
506+
strict: bool,
507+
) -> t.Tuple[t.Optional[str], t.Optional[str], t.Optional[int]]:
494508
try:
495509
subauthority = reference.authority_info()
496510
except exceptions.InvalidAuthority:

src/rfc3986/validators.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from . import misc
1919
from . import normalizers
2020
from . import uri
21+
from ._typing_compat import Self as _Self
2122

2223

2324
class Validator:
@@ -51,13 +52,13 @@ class Validator:
5152
["scheme", "userinfo", "host", "port", "path", "query", "fragment"]
5253
)
5354

54-
def __init__(self):
55+
def __init__(self) -> None:
5556
"""Initialize our default validations."""
56-
self.allowed_schemes: set[str] = set()
57-
self.allowed_hosts: set[str] = set()
58-
self.allowed_ports: set[str] = set()
59-
self.allow_password = True
60-
self.required_components = {
57+
self.allowed_schemes: t.Set[str] = set()
58+
self.allowed_hosts: t.Set[str] = set()
59+
self.allowed_ports: t.Set[str] = set()
60+
self.allow_password: bool = True
61+
self.required_components: t.Dict[str, bool] = {
6162
"scheme": False,
6263
"userinfo": False,
6364
"host": False,
@@ -66,9 +67,11 @@ def __init__(self):
6667
"query": False,
6768
"fragment": False,
6869
}
69-
self.validated_components = self.required_components.copy()
70+
self.validated_components: t.Dict[
71+
str, bool
72+
] = self.required_components.copy()
7073

71-
def allow_schemes(self, *schemes: str):
74+
def allow_schemes(self, *schemes: str) -> _Self:
7275
"""Require the scheme to be one of the provided schemes.
7376
7477
.. versionadded:: 1.0
@@ -84,7 +87,7 @@ def allow_schemes(self, *schemes: str):
8487
self.allowed_schemes.add(normalizers.normalize_scheme(scheme))
8588
return self
8689

87-
def allow_hosts(self, *hosts: str):
90+
def allow_hosts(self, *hosts: str) -> _Self:
8891
"""Require the host to be one of the provided hosts.
8992
9093
.. versionadded:: 1.0
@@ -100,7 +103,7 @@ def allow_hosts(self, *hosts: str):
100103
self.allowed_hosts.add(normalizers.normalize_host(host))
101104
return self
102105

103-
def allow_ports(self, *ports: str):
106+
def allow_ports(self, *ports: str) -> _Self:
104107
"""Require the port to be one of the provided ports.
105108
106109
.. versionadded:: 1.0
@@ -118,7 +121,7 @@ def allow_ports(self, *ports: str):
118121
self.allowed_ports.add(port)
119122
return self
120123

121-
def allow_use_of_password(self):
124+
def allow_use_of_password(self) -> _Self:
122125
"""Allow passwords to be present in the URI.
123126
124127
.. versionadded:: 1.0
@@ -131,7 +134,7 @@ def allow_use_of_password(self):
131134
self.allow_password = True
132135
return self
133136

134-
def forbid_use_of_password(self):
137+
def forbid_use_of_password(self) -> _Self:
135138
"""Prevent passwords from being included in the URI.
136139
137140
.. versionadded:: 1.0
@@ -144,7 +147,7 @@ def forbid_use_of_password(self):
144147
self.allow_password = False
145148
return self
146149

147-
def check_validity_of(self, *components: str):
150+
def check_validity_of(self, *components: str) -> _Self:
148151
"""Check the validity of the components provided.
149152
150153
This can be specified repeatedly.
@@ -167,7 +170,7 @@ def check_validity_of(self, *components: str):
167170
)
168171
return self
169172

170-
def require_presence_of(self, *components: str):
173+
def require_presence_of(self, *components: str) -> _Self:
171174
"""Require the components provided.
172175
173176
This can be specified repeatedly.
@@ -190,7 +193,7 @@ def require_presence_of(self, *components: str):
190193
)
191194
return self
192195

193-
def validate(self, uri: "uri.URIReference"):
196+
def validate(self, uri: "uri.URIReference") -> None:
194197
"""Check a URI for conditions specified on this validator.
195198
196199
.. versionadded:: 1.0
@@ -244,7 +247,7 @@ def check_password(uri: "uri.URIReference") -> None:
244247

245248

246249
def ensure_one_of(
247-
allowed_values: t.Container[object],
250+
allowed_values: t.Collection[object],
248251
uri: "uri.URIReference",
249252
attribute: str,
250253
) -> None:
@@ -261,7 +264,7 @@ def ensure_one_of(
261264
def ensure_required_components_exist(
262265
uri: "uri.URIReference",
263266
required_components: t.Iterable[str],
264-
):
267+
) -> None:
265268
"""Assert that all required components are present in the URI."""
266269
missing_components = sorted(
267270
component
@@ -294,7 +297,7 @@ def is_valid(
294297

295298

296299
def authority_is_valid(
297-
authority: str,
300+
authority: t.Optional[str],
298301
host: t.Optional[str] = None,
299302
require: bool = False,
300303
) -> bool:

0 commit comments

Comments
 (0)