Skip to content

Commit 75e77ba

Browse files
authored
Merge pull request #118 from Sachaa-Thanasius/feature/just-annotations
Add type annotations
2 parents b8ef055 + be1a4e8 commit 75e77ba

27 files changed

+627
-248
lines changed

.coveragerc

+2
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ exclude_lines =
33
.* # Python \d.*
44
.* # nocov: Python \d.*
55
.* # pragma: no cover.*
6+
^\s*(?:el)?if t\.TYPE_CHECKING:$
7+
^ +\.\.\.$
68
fail_under = 100

.github/workflows/main.yml

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ jobs:
1919
- os: windows-latest
2020
python: '3.12'
2121
toxenv: py
22+
# typing
23+
- os: ubuntu-latest
24+
python: '3.8'
25+
toxenv: typing
2226
# misc
2327
- os: ubuntu-latest
2428
python: '3.12'

.pre-commit-config.yaml

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.5.0
3+
rev: v4.6.0
44
hooks:
55
- id: check-yaml
66
- id: debug-statements
77
- id: end-of-file-fixer
88
- id: trailing-whitespace
9-
- repo: https://github.com/asottile/reorder-python-imports
10-
rev: v3.12.0
9+
- repo: https://github.com/pycqa/isort
10+
rev: 5.13.2
1111
hooks:
12-
- id: reorder-python-imports
13-
args: [--application-directories, '.:src', --py37-plus]
12+
- id: isort
1413
- repo: https://github.com/psf/black
15-
rev: 23.12.1
14+
rev: 24.4.2
1615
hooks:
1716
- id: black
18-
args: [--line-length=79]
1917
- repo: https://github.com/asottile/pyupgrade
20-
rev: v3.15.0
18+
rev: v3.16.0
2119
hooks:
2220
- id: pyupgrade
23-
args: [--py37-plus]
21+
args: [--py38-plus]
2422
- repo: https://github.com/pycqa/flake8
25-
rev: 7.0.0
23+
rev: 7.1.0
2624
hooks:
2725
- id: flake8
2826
exclude: ^(tests/|docs/|setup.py)
2927
additional_dependencies:
3028
- flake8-docstrings
31-
- flake8-import-order
3229
- repo: https://github.com/asottile/setup-cfg-fmt
3330
rev: v2.5.0
3431
hooks:

docs/source/conf.py

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
# sys.path.insert(0, os.path.abspath('.'))
2020
import rfc3986
2121

22-
2322
# -- General configuration ------------------------------------------------
2423

2524
# If your documentation needs a minimal Sphinx version, state it here.

pyproject.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[tool.black]
2+
line-length = 79
3+
target-version = ["py38"]
4+
5+
[tool.isort]
6+
profile = "black"
7+
line_length = 79
8+
force_single_line = true
9+
10+
[tool.pyright]
11+
include = ["src/rfc3986"]
12+
ignore = ["tests"]
13+
pythonVersion = "3.8"
14+
typeCheckingMode = "strict"
15+
16+
reportPrivateUsage = "none"
17+
reportImportCycles = "warning"
18+
reportPropertyTypeMismatch = "warning"
19+
reportUnnecessaryTypeIgnoreComment = "warning"

src/rfc3986/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
:copyright: (c) 2014 Rackspace
2020
:license: Apache v2.0, see LICENSE for details
2121
"""
22-
from .api import iri_reference
2322
from .api import IRIReference
23+
from .api import URIReference
24+
from .api import iri_reference
2425
from .api import is_valid_uri
2526
from .api import normalize_uri
2627
from .api import uri_reference
27-
from .api import URIReference
2828
from .api import urlparse
2929
from .parseresult import ParseResult
3030

src/rfc3986/_mixin.py

+50-25
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
"""Module containing the implementation of the URIMixin class."""
2+
3+
import typing as t
24
import warnings
35

46
from . import exceptions as exc
57
from . import misc
68
from . import normalizers
9+
from . import uri
710
from . import validators
11+
from ._typing_compat import Self as _Self
12+
13+
14+
class _AuthorityInfo(t.TypedDict):
15+
"""A typed dict for the authority info triple: userinfo, host, and port."""
16+
17+
userinfo: t.Optional[str]
18+
host: t.Optional[str]
19+
port: t.Optional[str]
820

921

1022
class URIMixin:
1123
"""Mixin with all shared methods for URIs and IRIs."""
1224

13-
__hash__ = tuple.__hash__
25+
if t.TYPE_CHECKING:
26+
scheme: t.Optional[str]
27+
authority: t.Optional[str]
28+
path: t.Optional[str]
29+
query: t.Optional[str]
30+
fragment: t.Optional[str]
31+
encoding: str
1432

15-
def authority_info(self):
33+
def authority_info(self) -> _AuthorityInfo:
1634
"""Return a dictionary with the ``userinfo``, ``host``, and ``port``.
1735
1836
If the authority is not valid, it will raise a
@@ -53,11 +71,11 @@ def authority_info(self):
5371

5472
return matches
5573

56-
def _match_subauthority(self):
74+
def _match_subauthority(self) -> t.Optional[t.Match[str]]:
5775
return misc.SUBAUTHORITY_MATCHER.match(self.authority)
5876

5977
@property
60-
def _validator(self):
78+
def _validator(self) -> validators.Validator:
6179
v = getattr(self, "_cached_validator", None)
6280
if v is not None:
6381
return v
@@ -67,7 +85,7 @@ def _validator(self):
6785
return self._cached_validator
6886

6987
@property
70-
def host(self):
88+
def host(self) -> t.Optional[str]:
7189
"""If present, a string representing the host."""
7290
try:
7391
authority = self.authority_info()
@@ -76,7 +94,7 @@ def host(self):
7694
return authority["host"]
7795

7896
@property
79-
def port(self):
97+
def port(self) -> t.Optional[str]:
8098
"""If present, the port extracted from the authority."""
8199
try:
82100
authority = self.authority_info()
@@ -85,15 +103,15 @@ def port(self):
85103
return authority["port"]
86104

87105
@property
88-
def userinfo(self):
106+
def userinfo(self) -> t.Optional[str]:
89107
"""If present, the userinfo extracted from the authority."""
90108
try:
91109
authority = self.authority_info()
92110
except exc.InvalidAuthority:
93111
return None
94112
return authority["userinfo"]
95113

96-
def is_absolute(self):
114+
def is_absolute(self) -> bool:
97115
"""Determine if this URI Reference is an absolute URI.
98116
99117
See http://tools.ietf.org/html/rfc3986#section-4.3 for explanation.
@@ -103,7 +121,7 @@ def is_absolute(self):
103121
"""
104122
return bool(misc.ABSOLUTE_URI_MATCHER.match(self.unsplit()))
105123

106-
def is_valid(self, **kwargs):
124+
def is_valid(self, **kwargs: bool) -> bool:
107125
"""Determine if the URI is valid.
108126
109127
.. deprecated:: 1.1.0
@@ -137,7 +155,7 @@ def is_valid(self, **kwargs):
137155
]
138156
return all(v(r) for v, r in validators)
139157

140-
def authority_is_valid(self, require=False):
158+
def authority_is_valid(self, require: bool = False) -> bool:
141159
"""Determine if the authority component is valid.
142160
143161
.. deprecated:: 1.1.0
@@ -167,7 +185,7 @@ def authority_is_valid(self, require=False):
167185
require=require,
168186
)
169187

170-
def scheme_is_valid(self, require=False):
188+
def scheme_is_valid(self, require: bool = False) -> bool:
171189
"""Determine if the scheme component is valid.
172190
173191
.. deprecated:: 1.1.0
@@ -186,7 +204,7 @@ def scheme_is_valid(self, require=False):
186204
)
187205
return validators.scheme_is_valid(self.scheme, require)
188206

189-
def path_is_valid(self, require=False):
207+
def path_is_valid(self, require: bool = False) -> bool:
190208
"""Determine if the path component is valid.
191209
192210
.. deprecated:: 1.1.0
@@ -205,7 +223,7 @@ def path_is_valid(self, require=False):
205223
)
206224
return validators.path_is_valid(self.path, require)
207225

208-
def query_is_valid(self, require=False):
226+
def query_is_valid(self, require: bool = False) -> bool:
209227
"""Determine if the query component is valid.
210228
211229
.. deprecated:: 1.1.0
@@ -224,7 +242,7 @@ def query_is_valid(self, require=False):
224242
)
225243
return validators.query_is_valid(self.query, require)
226244

227-
def fragment_is_valid(self, require=False):
245+
def fragment_is_valid(self, require: bool = False) -> bool:
228246
"""Determine if the fragment component is valid.
229247
230248
.. deprecated:: 1.1.0
@@ -243,7 +261,7 @@ def fragment_is_valid(self, require=False):
243261
)
244262
return validators.fragment_is_valid(self.fragment, require)
245263

246-
def normalized_equality(self, other_ref):
264+
def normalized_equality(self, other_ref: "uri.URIReference") -> bool:
247265
"""Compare this URIReference to another URIReference.
248266
249267
:param URIReference other_ref: (required), The reference with which
@@ -253,7 +271,11 @@ def normalized_equality(self, other_ref):
253271
"""
254272
return tuple(self.normalize()) == tuple(other_ref.normalize())
255273

256-
def resolve_with(self, base_uri, strict=False):
274+
def resolve_with( # noqa: C901
275+
self,
276+
base_uri: t.Union[str, "uri.URIReference"],
277+
strict: bool = False,
278+
) -> _Self:
257279
"""Use an absolute URI Reference to resolve this relative reference.
258280
259281
Assuming this is a relative reference that you would like to resolve,
@@ -272,6 +294,9 @@ def resolve_with(self, base_uri, strict=False):
272294
if not isinstance(base_uri, URIMixin):
273295
base_uri = type(self).from_string(base_uri)
274296

297+
if t.TYPE_CHECKING:
298+
base_uri = t.cast(uri.URIReference, base_uri)
299+
275300
try:
276301
self._validator.validate(base_uri)
277302
except exc.ValidationError:
@@ -325,14 +350,14 @@ def resolve_with(self, base_uri, strict=False):
325350
)
326351
return target
327352

328-
def unsplit(self):
353+
def unsplit(self) -> str:
329354
"""Create a URI string from the components.
330355
331356
:returns: The URI Reference reconstituted as a string.
332357
:rtype: str
333358
"""
334359
# See http://tools.ietf.org/html/rfc3986#section-5.3
335-
result_list = []
360+
result_list: list[str] = []
336361
if self.scheme:
337362
result_list.extend([self.scheme, ":"])
338363
if self.authority:
@@ -347,12 +372,12 @@ def unsplit(self):
347372

348373
def copy_with(
349374
self,
350-
scheme=misc.UseExisting,
351-
authority=misc.UseExisting,
352-
path=misc.UseExisting,
353-
query=misc.UseExisting,
354-
fragment=misc.UseExisting,
355-
):
375+
scheme: t.Optional[str] = misc.UseExisting,
376+
authority: t.Optional[str] = misc.UseExisting,
377+
path: t.Optional[str] = misc.UseExisting,
378+
query: t.Optional[str] = misc.UseExisting,
379+
fragment: t.Optional[str] = misc.UseExisting,
380+
) -> _Self:
356381
"""Create a copy of this reference with the new components.
357382
358383
:param str scheme:
@@ -380,6 +405,6 @@ def copy_with(
380405
for key, value in list(attributes.items()):
381406
if value is misc.UseExisting:
382407
del attributes[key]
383-
uri = self._replace(**attributes)
408+
uri: _Self = self._replace(**attributes)
384409
uri.encoding = self.encoding
385410
return uri

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): # pragma: no cover
7+
from typing import Self
8+
elif t.TYPE_CHECKING:
9+
from typing_extensions import Self
10+
else: # pragma: no cover
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"."""

0 commit comments

Comments
 (0)