uritools 6.1.2__tar.gz → 6.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {uritools-6.1.2 → uritools-6.1.4}/CHANGELOG.rst +24 -0
- {uritools-6.1.2/src/uritools.egg-info → uritools-6.1.4}/PKG-INFO +2 -1
- uritools-6.1.4/docs/conf.py +18 -0
- {uritools-6.1.2 → uritools-6.1.4}/docs/index.rst +25 -1
- {uritools-6.1.2 → uritools-6.1.4}/pyproject.toml +5 -2
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools/__init__.py +44 -25
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools/__init__.pyi +3 -4
- {uritools-6.1.2 → uritools-6.1.4/src/uritools.egg-info}/PKG-INFO +2 -1
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_compose.py +76 -9
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_defrag.py +13 -1
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_encoding.py +1 -1
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_join.py +8 -0
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_split.py +33 -1
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_unsplit.py +14 -0
- {uritools-6.1.2 → uritools-6.1.4}/tox.ini +8 -11
- uritools-6.1.2/docs/conf.py +0 -36
- {uritools-6.1.2 → uritools-6.1.4}/LICENSE +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/MANIFEST.in +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/README.rst +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/setup.cfg +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools/py.typed +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools.egg-info/SOURCES.txt +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools.egg-info/dependency_links.txt +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/src/uritools.egg-info/top_level.txt +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/tests/__init__.py +0 -0
- {uritools-6.1.2 → uritools-6.1.4}/tests/test_classify.py +0 -0
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
v6.1.4 (2026-11-04)
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
- Fix newline handling in ``uricompose()`` and ``urisplit()``.
|
|
5
|
+
|
|
6
|
+
- Compare schemes case-insensitively in non-strict ``urijoin()``.
|
|
7
|
+
|
|
8
|
+
- Recognize an uppercase ``IPvFuture`` version flag.
|
|
9
|
+
|
|
10
|
+
- Various documentation improvements.
|
|
11
|
+
|
|
12
|
+
- Minor unit test improvements.
|
|
13
|
+
|
|
14
|
+
- Update build environment.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
v6.1.3 (2026-07-24)
|
|
18
|
+
===================
|
|
19
|
+
|
|
20
|
+
- Minor style improvements to keep ``ruff`` happy.
|
|
21
|
+
|
|
22
|
+
- Update build environment.
|
|
23
|
+
|
|
24
|
+
|
|
1
25
|
v6.1.2 (2026-05-22)
|
|
2
26
|
===================
|
|
3
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uritools
|
|
3
|
-
Version: 6.1.
|
|
3
|
+
Version: 6.1.4
|
|
4
4
|
Summary: URI parsing, classification and composition
|
|
5
5
|
Author-email: Thomas Kemmer <tkemmer@computer.org>
|
|
6
6
|
Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
26
|
Requires-Python: >=3.10
|
|
26
27
|
Description-Content-Type: text/x-rst
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
project = "uritools"
|
|
4
|
+
copyright = "2014-2026 Thomas Kemmer"
|
|
5
|
+
release = importlib.metadata.version(project)
|
|
6
|
+
version = ".".join(release.split(".")[:2])
|
|
7
|
+
|
|
8
|
+
extensions = [
|
|
9
|
+
"sphinx.ext.autodoc",
|
|
10
|
+
"sphinx.ext.coverage",
|
|
11
|
+
"sphinx.ext.doctest",
|
|
12
|
+
"sphinx.ext.intersphinx",
|
|
13
|
+
"sphinx.ext.todo",
|
|
14
|
+
]
|
|
15
|
+
exclude_patterns = ["_build"]
|
|
16
|
+
root_doc = "index"
|
|
17
|
+
html_theme = "classic"
|
|
18
|
+
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
|
@@ -102,6 +102,9 @@ URI Composition
|
|
|
102
102
|
|
|
103
103
|
All components may be specified as either Unicode strings, which
|
|
104
104
|
will be encoded according to `encoding`, or :class:`bytes` objects.
|
|
105
|
+
A `host` given as a Unicode string is always encoded as UTF-8,
|
|
106
|
+
regardless of `encoding`, as :rfc:`3986#section-3.2.2` requires
|
|
107
|
+
for non-ASCII registered names.
|
|
105
108
|
|
|
106
109
|
`authority` may also be passed a three-item sequence specifying
|
|
107
110
|
userinfo, host and port subcomponents. If both `authority` and any
|
|
@@ -118,10 +121,15 @@ URI Composition
|
|
|
118
121
|
.. autofunction:: urijoin
|
|
119
122
|
|
|
120
123
|
If `strict` is :const:`False`, a scheme in the reference is
|
|
121
|
-
ignored if it
|
|
124
|
+
ignored if it matches the base URI's scheme, ignoring case.
|
|
122
125
|
|
|
123
126
|
.. autofunction:: uriunsplit
|
|
124
127
|
|
|
128
|
+
The type of the returned URI reference is that of the `path`
|
|
129
|
+
element. All other elements must be of the same type, or
|
|
130
|
+
:const:`None`; mixing :class:`str` and :class:`bytes` elements
|
|
131
|
+
raises :exc:`TypeError`.
|
|
132
|
+
|
|
125
133
|
|
|
126
134
|
URI Decomposition
|
|
127
135
|
=================
|
|
@@ -187,6 +195,11 @@ URI Encoding
|
|
|
187
195
|
percent-encodings and decode `uristring` using the codec registered
|
|
188
196
|
for `encoding`, returning a Unicode string.
|
|
189
197
|
|
|
198
|
+
`errors` specifies how decoding errors are handled, as for
|
|
199
|
+
:meth:`bytes.decode`. A Unicode `uristring` is first encoded as
|
|
200
|
+
ASCII if `encoding` is :const:`None`, and using `encoding`
|
|
201
|
+
otherwise, with the same `errors` setting.
|
|
202
|
+
|
|
190
203
|
.. autofunction:: uriencode
|
|
191
204
|
|
|
192
205
|
If `uristring` is a :class:`bytes` object, replace any characters
|
|
@@ -195,6 +208,12 @@ URI Encoding
|
|
|
195
208
|
Otherwise, encode `uristring` using the codec registered for
|
|
196
209
|
`encoding` before replacing any percent encodings.
|
|
197
210
|
|
|
211
|
+
`safe` specifies additional characters that are left unencoded, as
|
|
212
|
+
a string of ASCII characters or a :class:`bytes` object, for
|
|
213
|
+
example ``"/"`` to keep path separators. `encoding` and `errors`
|
|
214
|
+
are passed to :meth:`str.encode` when `uristring` is a Unicode
|
|
215
|
+
string; they are ignored for :class:`bytes` input.
|
|
216
|
+
|
|
198
217
|
|
|
199
218
|
Structured Parse Results
|
|
200
219
|
========================
|
|
@@ -211,6 +230,11 @@ convenience methods.
|
|
|
211
230
|
.. autoclass:: SplitResult
|
|
212
231
|
:members:
|
|
213
232
|
|
|
233
|
+
:meth:`transform` expects `ref` to be of the same type as the
|
|
234
|
+
components of the result, i.e. a string or a :class:`bytes`
|
|
235
|
+
object; :func:`urijoin` converts mixed `base` and `ref` types
|
|
236
|
+
itself.
|
|
237
|
+
|
|
214
238
|
|
|
215
239
|
Character Constants
|
|
216
240
|
===================
|
|
@@ -27,6 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
28
|
"Programming Language :: Python :: 3.13",
|
|
29
29
|
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Programming Language :: Python :: 3.15",
|
|
30
31
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
32
|
]
|
|
32
33
|
|
|
@@ -49,5 +50,7 @@ version = {attr = "uritools.__version__"}
|
|
|
49
50
|
|
|
50
51
|
[tool.pyright]
|
|
51
52
|
typeCheckingMode = "standard"
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
ignore = ["DTZ005", "SIM102", "UP031"]
|
|
@@ -33,7 +33,7 @@ __all__ = (
|
|
|
33
33
|
"uriunsplit",
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
__version__ = "6.1.
|
|
36
|
+
__version__ = "6.1.4"
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
# RFC 3986 2.2. Reserved Characters
|
|
@@ -74,7 +74,7 @@ def uriencode(uristring, safe="", encoding="utf-8", errors="strict"):
|
|
|
74
74
|
"""Encode a URI string or string component."""
|
|
75
75
|
if not isinstance(uristring, bytes):
|
|
76
76
|
uristring = uristring.encode(encoding, errors)
|
|
77
|
-
if
|
|
77
|
+
if isinstance(safe, str):
|
|
78
78
|
safe = safe.encode("ascii")
|
|
79
79
|
# FIXME: though unlikely, _encoded may grow without bounds if arbitrary
|
|
80
80
|
# safe values are passed by a caller - consider using @functools.lru_cache
|
|
@@ -84,7 +84,7 @@ def uriencode(uristring, safe="", encoding="utf-8", errors="strict"):
|
|
|
84
84
|
except KeyError:
|
|
85
85
|
encoded = _encoded[b""][:]
|
|
86
86
|
for i in safe:
|
|
87
|
-
encoded[i] = bytes([i])
|
|
87
|
+
encoded[i] = bytes([i])
|
|
88
88
|
_encoded[safe] = encoded
|
|
89
89
|
return b"".join(map(encoded.__getitem__, uristring))
|
|
90
90
|
|
|
@@ -141,6 +141,21 @@ class SplitResult(
|
|
|
141
141
|
|
|
142
142
|
__slots__ = () # prevent creation of instance dictionary
|
|
143
143
|
|
|
144
|
+
# regex and delimiter constants, defined by SplitResultBytes and SplitResultString
|
|
145
|
+
_RE: re.Pattern
|
|
146
|
+
_COLON: str | bytes
|
|
147
|
+
_SLASH: str | bytes
|
|
148
|
+
_QUEST: str | bytes
|
|
149
|
+
_HASH: str | bytes
|
|
150
|
+
_LBRACKET: str | bytes
|
|
151
|
+
_RBRACKET: str | bytes
|
|
152
|
+
_AT: str | bytes
|
|
153
|
+
_DOT: str | bytes
|
|
154
|
+
_DOTDOT: str | bytes
|
|
155
|
+
_EMPTY: str | bytes
|
|
156
|
+
_EQ: str | bytes
|
|
157
|
+
_DIGITS: str | bytes
|
|
158
|
+
|
|
144
159
|
@property
|
|
145
160
|
def userinfo(self):
|
|
146
161
|
authority = self.authority
|
|
@@ -260,6 +275,10 @@ class SplitResult(
|
|
|
260
275
|
else:
|
|
261
276
|
return ipaddress.IPv4Address(host)
|
|
262
277
|
except ValueError:
|
|
278
|
+
# RFC 3986 3.2.2. Host: Non-ASCII characters must first be
|
|
279
|
+
# encoded according to UTF-8 [STD63], and then each octet
|
|
280
|
+
# of the corresponding UTF-8 sequence must be
|
|
281
|
+
# percent-encoded to be represented as URI characters.
|
|
263
282
|
return uridecode(host, "utf-8", errors).lower()
|
|
264
283
|
|
|
265
284
|
def getport(self, default=None):
|
|
@@ -381,8 +400,11 @@ class SplitResult(
|
|
|
381
400
|
"""
|
|
382
401
|
scheme, authority, path, query, fragment = self._match(ref).groups()
|
|
383
402
|
|
|
384
|
-
# RFC 3986 5.2.2. Transform References
|
|
385
|
-
|
|
403
|
+
# RFC 3986 5.2.2. Transform References; scheme names are
|
|
404
|
+
# case-insensitive (RFC 3986 3.1)
|
|
405
|
+
if scheme is not None and (
|
|
406
|
+
strict or self.scheme is None or scheme.lower() != self.scheme.lower()
|
|
407
|
+
):
|
|
386
408
|
path = self.__remove_dot_segments(path)
|
|
387
409
|
elif authority is not None:
|
|
388
410
|
scheme = self.scheme
|
|
@@ -402,6 +424,10 @@ class SplitResult(
|
|
|
402
424
|
path = self.__remove_dot_segments(self.__merge(path))
|
|
403
425
|
return type(self)(scheme, authority, path, query, fragment)
|
|
404
426
|
|
|
427
|
+
@classmethod
|
|
428
|
+
def _match(cls, ref):
|
|
429
|
+
return cls._RE.match(ref)
|
|
430
|
+
|
|
405
431
|
def __merge(self, path):
|
|
406
432
|
# RFC 3986 5.2.3. Merge Paths
|
|
407
433
|
if self.authority is not None and not self.path:
|
|
@@ -451,8 +477,8 @@ class SplitResult(
|
|
|
451
477
|
# not supported".
|
|
452
478
|
if isinstance(address, bytes):
|
|
453
479
|
address = address.decode("ascii")
|
|
454
|
-
if address.startswith("v"):
|
|
455
|
-
raise ValueError("
|
|
480
|
+
if address.startswith(("v", "V")):
|
|
481
|
+
raise ValueError("Address mechanism not supported")
|
|
456
482
|
return ipaddress.IPv6Address(address)
|
|
457
483
|
|
|
458
484
|
|
|
@@ -469,13 +495,9 @@ class SplitResultBytes(SplitResult):
|
|
|
469
495
|
(?:\?([^#]*))? # query
|
|
470
496
|
(?:\#(.*))? # fragment
|
|
471
497
|
""",
|
|
472
|
-
flags=re.VERBOSE,
|
|
498
|
+
flags=re.VERBOSE | re.DOTALL,
|
|
473
499
|
)
|
|
474
500
|
|
|
475
|
-
@classmethod
|
|
476
|
-
def _match(cls, ref):
|
|
477
|
-
return cls._RE.match(ref)
|
|
478
|
-
|
|
479
501
|
# RFC 3986 2.2 gen-delims
|
|
480
502
|
_COLON, _SLASH, _QUEST, _HASH, _LBRACKET, _RBRACKET, _AT = (
|
|
481
503
|
b":",
|
|
@@ -508,13 +530,9 @@ class SplitResultString(SplitResult):
|
|
|
508
530
|
(?:\?([^#]*))? # query
|
|
509
531
|
(?:\#(.*))? # fragment
|
|
510
532
|
""",
|
|
511
|
-
flags=re.VERBOSE,
|
|
533
|
+
flags=re.VERBOSE | re.DOTALL,
|
|
512
534
|
)
|
|
513
535
|
|
|
514
|
-
@classmethod
|
|
515
|
-
def _match(cls, ref):
|
|
516
|
-
return cls._RE.match(ref)
|
|
517
|
-
|
|
518
536
|
# RFC 3986 2.2 gen-delims
|
|
519
537
|
_COLON, _SLASH, _QUEST, _HASH, _LBRACKET, _RBRACKET, _AT = (
|
|
520
538
|
":",
|
|
@@ -614,11 +632,11 @@ def issamedoc(uristring):
|
|
|
614
632
|
|
|
615
633
|
|
|
616
634
|
# RFC 3986 3.1: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
|
617
|
-
_SCHEME_RE = re.compile(b"
|
|
635
|
+
_SCHEME_RE = re.compile(b"[A-Za-z][A-Za-z0-9+.-]*")
|
|
618
636
|
|
|
619
637
|
# RFC 3986 3.2: authority = [ userinfo "@" ] host [ ":" port ]
|
|
620
|
-
_AUTHORITY_RE_BYTES = re.compile(b"
|
|
621
|
-
_AUTHORITY_RE_STR = re.compile("
|
|
638
|
+
_AUTHORITY_RE_BYTES = re.compile(b"(?:(.*)@)?(.*?)(?::([0-9]*))?", re.DOTALL)
|
|
639
|
+
_AUTHORITY_RE_STR = re.compile("(?:(.*)@)?(.*?)(?::([0-9]*))?", re.DOTALL)
|
|
622
640
|
|
|
623
641
|
# safe component characters
|
|
624
642
|
_SAFE_USERINFO = SUB_DELIMS + ":"
|
|
@@ -629,7 +647,7 @@ _SAFE_FRAGMENT = SUB_DELIMS + ":@/?"
|
|
|
629
647
|
|
|
630
648
|
|
|
631
649
|
def _scheme(scheme):
|
|
632
|
-
if _SCHEME_RE.
|
|
650
|
+
if _SCHEME_RE.fullmatch(scheme):
|
|
633
651
|
return scheme.lower()
|
|
634
652
|
else:
|
|
635
653
|
raise ValueError("Invalid scheme component")
|
|
@@ -664,7 +682,7 @@ def _authority(userinfo, host, port, encoding):
|
|
|
664
682
|
|
|
665
683
|
|
|
666
684
|
def _ip_literal(address):
|
|
667
|
-
if address.startswith("v"):
|
|
685
|
+
if address.startswith(("v", "V")):
|
|
668
686
|
raise ValueError("Address mechanism not supported")
|
|
669
687
|
else:
|
|
670
688
|
return b"[" + ipaddress.IPv6Address(address).compressed.encode() + b"]"
|
|
@@ -755,9 +773,9 @@ def uricompose(
|
|
|
755
773
|
if authority is None:
|
|
756
774
|
authority = (None, None, None)
|
|
757
775
|
elif isinstance(authority, bytes):
|
|
758
|
-
authority = _AUTHORITY_RE_BYTES.
|
|
776
|
+
authority = _AUTHORITY_RE_BYTES.fullmatch(authority).groups() # type: ignore
|
|
759
777
|
elif isinstance(authority, str):
|
|
760
|
-
authority = _AUTHORITY_RE_STR.
|
|
778
|
+
authority = _AUTHORITY_RE_STR.fullmatch(authority).groups() # type: ignore
|
|
761
779
|
elif not isinstance(authority, collections.abc.Sequence):
|
|
762
780
|
raise TypeError("Invalid authority type")
|
|
763
781
|
elif len(authority) != 3:
|
|
@@ -817,4 +835,5 @@ def uricompose(
|
|
|
817
835
|
fragment = uriencode(fragment, _SAFE_FRAGMENT, encoding)
|
|
818
836
|
|
|
819
837
|
# return URI reference as `str`
|
|
820
|
-
|
|
838
|
+
parts = (scheme, authority, path, query, fragment)
|
|
839
|
+
return uriunsplit(parts).decode() # type: ignore
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import ipaddress
|
|
2
2
|
from collections.abc import Iterable, Mapping, Sequence
|
|
3
|
-
from typing import Any, AnyStr, Generic, NamedTuple, overload
|
|
4
|
-
from typing_extensions import TypeAlias
|
|
3
|
+
from typing import Any, AnyStr, Generic, NamedTuple, TypeAlias, overload
|
|
5
4
|
|
|
6
|
-
__all__ =
|
|
5
|
+
__all__ = (
|
|
7
6
|
"GEN_DELIMS",
|
|
8
7
|
"RESERVED",
|
|
9
8
|
"SUB_DELIMS",
|
|
@@ -21,7 +20,7 @@ __all__ = [
|
|
|
21
20
|
"urijoin",
|
|
22
21
|
"urisplit",
|
|
23
22
|
"uriunsplit",
|
|
24
|
-
|
|
23
|
+
)
|
|
25
24
|
__version__: str
|
|
26
25
|
|
|
27
26
|
GEN_DELIMS: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uritools
|
|
3
|
-
Version: 6.1.
|
|
3
|
+
Version: 6.1.4
|
|
4
4
|
Summary: URI parsing, classification and composition
|
|
5
5
|
Author-email: Thomas Kemmer <tkemmer@computer.org>
|
|
6
6
|
Maintainer-email: Thomas Kemmer <tkemmer@computer.org>
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
26
|
Requires-Python: >=3.10
|
|
26
27
|
Description-Content-Type: text/x-rst
|
|
@@ -37,7 +37,7 @@ class ComposeTest(unittest.TestCase):
|
|
|
37
37
|
for uri, scheme in cases:
|
|
38
38
|
self.check(uri, scheme=scheme)
|
|
39
39
|
# invalid scheme
|
|
40
|
-
for scheme in ("", "foo:", "\xf6lk\xfcrbis"):
|
|
40
|
+
for scheme in ("", "foo:", "foo\n", b"foo\n", "\xf6lk\xfcrbis"):
|
|
41
41
|
with self.assertRaises(ValueError, msg="scheme=%r" % scheme):
|
|
42
42
|
uricompose(scheme=scheme)
|
|
43
43
|
|
|
@@ -62,6 +62,12 @@ class ComposeTest(unittest.TestCase):
|
|
|
62
62
|
("//user@[::1]:42", b"user@[::1]:42"),
|
|
63
63
|
("//user:c2VjcmV0@example.com", "user:c2VjcmV0@example.com"),
|
|
64
64
|
("//user:c2VjcmV0@example.com", b"user:c2VjcmV0@example.com"),
|
|
65
|
+
("//foo%0Abar", "foo\nbar"),
|
|
66
|
+
("//foo%0Abar", b"foo\nbar"),
|
|
67
|
+
("//a%0Ab@example.com", "a\nb@example.com"),
|
|
68
|
+
("//a%0Ab@example.com", b"a\nb@example.com"),
|
|
69
|
+
("//example.com%3A42%0A", "example.com:42\n"),
|
|
70
|
+
("//example.com%3A42%0A", b"example.com:42\n"),
|
|
65
71
|
]
|
|
66
72
|
for uri, authority in cases:
|
|
67
73
|
self.check(uri, authority=authority)
|
|
@@ -120,11 +126,15 @@ class ComposeTest(unittest.TestCase):
|
|
|
120
126
|
with self.assertRaises(TypeError, msg="host=%r" % host):
|
|
121
127
|
uricompose(host=host) # type: ignore
|
|
122
128
|
# invalid host ip-literal
|
|
123
|
-
for host in ("[foo]", "[v1.x]"):
|
|
129
|
+
for host in ("[foo]", "[v1.x]", "[V1.x]"):
|
|
124
130
|
with self.assertRaises(ValueError, msg="host=%r" % host):
|
|
125
131
|
uricompose(authority=[None, host, None])
|
|
126
132
|
with self.assertRaises(ValueError, msg="host=%r" % host):
|
|
127
133
|
uricompose(host=host)
|
|
134
|
+
# RFC 3986 3.2.2: the IPvFuture version flag is case-insensitive
|
|
135
|
+
for host in ("[v1.x]", "[V1.x]", b"[v1.x]", b"[V1.x]"):
|
|
136
|
+
with self.assertRaisesRegex(ValueError, "mechanism not supported"):
|
|
137
|
+
uricompose(host=host)
|
|
128
138
|
# invalid port value
|
|
129
139
|
for port in (-1, "foo", 3.14):
|
|
130
140
|
with self.assertRaises(ValueError, msg="port=%r" % port):
|
|
@@ -138,6 +148,25 @@ class ComposeTest(unittest.TestCase):
|
|
|
138
148
|
("//user@example.com:42", "", "user", "example.com", 42),
|
|
139
149
|
("//user@example.com:42", "example.com", "user", None, 42),
|
|
140
150
|
("//user@example.com:42", "user@:42", None, "example.com", None),
|
|
151
|
+
("//user@example.com:42", b"", b"user", b"example.com", b"42"),
|
|
152
|
+
("//user@example.com:42", b"example.com", b"user", None, 42),
|
|
153
|
+
("//user@example.com:42", b"user@:42", None, b"example.com", None),
|
|
154
|
+
("//user@example.com:42", b"user@:42", None, "example.com", None),
|
|
155
|
+
("//user@example.com:42", "user@:42", None, b"example.com", None),
|
|
156
|
+
(
|
|
157
|
+
"//user@example.com:42",
|
|
158
|
+
b"nobody@localhost:80",
|
|
159
|
+
"user",
|
|
160
|
+
b"example.com",
|
|
161
|
+
"42",
|
|
162
|
+
),
|
|
163
|
+
(
|
|
164
|
+
"//user@example.com:42",
|
|
165
|
+
"nobody@localhost:80",
|
|
166
|
+
b"user",
|
|
167
|
+
"example.com",
|
|
168
|
+
b"42",
|
|
169
|
+
),
|
|
141
170
|
]
|
|
142
171
|
for uri, authority, userinfo, host, port in cases:
|
|
143
172
|
self.check(
|
|
@@ -212,13 +241,13 @@ class ComposeTest(unittest.TestCase):
|
|
|
212
241
|
("?name=foo&type=bar", od([("name", "foo"), ("type", "bar")])),
|
|
213
242
|
("?name=foo&name=bar", [("name", "foo"), ("name", "bar")]),
|
|
214
243
|
("?name=foo&name=bar", {"name": ["foo", "bar"]}),
|
|
215
|
-
("?name=a/b/c",
|
|
216
|
-
("?name=a:b:c",
|
|
217
|
-
("?name=a?b?c",
|
|
218
|
-
("?name=a@b@c",
|
|
219
|
-
("?name=a;b;c",
|
|
220
|
-
("?name=a%23b%23c",
|
|
221
|
-
("?name=a%26b%26c",
|
|
244
|
+
("?name=a/b/c", {"name": "a/b/c"}),
|
|
245
|
+
("?name=a:b:c", {"name": "a:b:c"}),
|
|
246
|
+
("?name=a?b?c", {"name": "a?b?c"}),
|
|
247
|
+
("?name=a@b@c", {"name": "a@b@c"}),
|
|
248
|
+
("?name=a;b;c", {"name": "a;b;c"}),
|
|
249
|
+
("?name=a%23b%23c", {"name": "a#b#c"}),
|
|
250
|
+
("?name=a%26b%26c", {"name": "a&b&c"}),
|
|
222
251
|
]
|
|
223
252
|
for uri, query in cases:
|
|
224
253
|
self.check(uri, query=query)
|
|
@@ -236,3 +265,41 @@ class ComposeTest(unittest.TestCase):
|
|
|
236
265
|
]
|
|
237
266
|
for sep, uri, query in cases:
|
|
238
267
|
self.check(uri, query=query, querysep=sep)
|
|
268
|
+
|
|
269
|
+
def test_encoding(self):
|
|
270
|
+
cases = [
|
|
271
|
+
("//%E4@example.com", {"userinfo": "\xe4", "host": "example.com"}),
|
|
272
|
+
("/%E4", {"path": "/\xe4"}),
|
|
273
|
+
("?name=%E4", {"query": "name=\xe4"}),
|
|
274
|
+
("?name=%E4", {"query": {"name": "\xe4"}}),
|
|
275
|
+
("?name=%E4", {"query": [("name", "\xe4")]}),
|
|
276
|
+
("#%E4", {"fragment": "\xe4"}),
|
|
277
|
+
]
|
|
278
|
+
for uri, kwargs in cases:
|
|
279
|
+
self.check(uri, encoding="latin-1", **kwargs)
|
|
280
|
+
# host is always encoded as UTF-8
|
|
281
|
+
self.check("//%C3%A4", host="\xe4", encoding="latin-1")
|
|
282
|
+
# bytes components are not affected by encoding
|
|
283
|
+
self.check("/%C3%A4", path=b"/\xc3\xa4", encoding="latin-1")
|
|
284
|
+
# characters not representable in encoding
|
|
285
|
+
with self.assertRaises(UnicodeEncodeError):
|
|
286
|
+
uricompose(path="/\u20ac", encoding="latin-1")
|
|
287
|
+
|
|
288
|
+
def test_fragment(self):
|
|
289
|
+
cases = [
|
|
290
|
+
("", None),
|
|
291
|
+
("#", ""),
|
|
292
|
+
("#", b""),
|
|
293
|
+
("#nose", "nose"),
|
|
294
|
+
("#nose", b"nose"),
|
|
295
|
+
("#foo%20bar", "foo bar"),
|
|
296
|
+
("#foo%20bar", b"foo bar"),
|
|
297
|
+
("#a/b?c:d@e", "a/b?c:d@e"),
|
|
298
|
+
("#a/b?c:d@e", b"a/b?c:d@e"),
|
|
299
|
+
("#a%23b", "a#b"),
|
|
300
|
+
("#a%23b", b"a#b"),
|
|
301
|
+
("#%C3%A4", "\xe4"),
|
|
302
|
+
("#%C3%A4", b"\xc3\xa4"),
|
|
303
|
+
]
|
|
304
|
+
for uri, fragment in cases:
|
|
305
|
+
self.check(uri, fragment=fragment)
|
|
@@ -23,7 +23,7 @@ class DefragTest(unittest.TestCase):
|
|
|
23
23
|
def encode(s):
|
|
24
24
|
return s.encode() if s is not None else None
|
|
25
25
|
|
|
26
|
-
cases +=
|
|
26
|
+
cases += [map(encode, case) for case in cases]
|
|
27
27
|
|
|
28
28
|
for uri, base, fragment in cases:
|
|
29
29
|
defrag = uridefrag(uri)
|
|
@@ -42,6 +42,18 @@ class DefragTest(unittest.TestCase):
|
|
|
42
42
|
self.assertEqual(uridefrag("#foo%20bar").getfragment(), "foo bar")
|
|
43
43
|
self.assertEqual(uridefrag(b"#foo%20bar").getfragment(), "foo bar")
|
|
44
44
|
|
|
45
|
+
def test_getfragment_default(self):
|
|
46
|
+
self.assertEqual(uridefrag("").getfragment("baz"), "baz")
|
|
47
|
+
self.assertEqual(uridefrag(b"").getfragment("baz"), "baz")
|
|
48
|
+
self.assertEqual(uridefrag("").getfragment(default="baz"), "baz")
|
|
49
|
+
self.assertEqual(uridefrag(b"").getfragment(default="baz"), "baz")
|
|
50
|
+
self.assertEqual(uridefrag("#").getfragment("baz"), "")
|
|
51
|
+
self.assertEqual(uridefrag(b"#").getfragment("baz"), "")
|
|
52
|
+
self.assertEqual(uridefrag("#foo").getfragment("baz"), "foo")
|
|
53
|
+
self.assertEqual(uridefrag(b"#foo").getfragment("baz"), "foo")
|
|
54
|
+
self.assertEqual(uridefrag("#foo%20bar").getfragment("baz"), "foo bar")
|
|
55
|
+
self.assertEqual(uridefrag(b"#foo%20bar").getfragment("baz"), "foo bar")
|
|
56
|
+
|
|
45
57
|
def test_getfragment_encoding_none(self):
|
|
46
58
|
self.assertEqual(uridefrag("").getfragment(encoding=None), None)
|
|
47
59
|
self.assertEqual(uridefrag(b"").getfragment(encoding=None), None)
|
|
@@ -8,7 +8,7 @@ class EncodingTest(unittest.TestCase):
|
|
|
8
8
|
self.assertEqual(uriencode(decoded, safe, encoding), encoded)
|
|
9
9
|
self.assertEqual(uridecode(encoded, encoding), decoded)
|
|
10
10
|
# swap bytes/string types
|
|
11
|
-
self.assertEqual(uriencode(decoded.encode(encoding), safe, encoding), encoded)
|
|
11
|
+
self.assertEqual(uriencode(decoded.encode(encoding), safe, encoding), encoded)
|
|
12
12
|
self.assertEqual(uridecode(encoded.decode("ascii"), encoding), decoded)
|
|
13
13
|
|
|
14
14
|
def test_encoding(self):
|
|
@@ -66,6 +66,14 @@ class JoinTest(unittest.TestCase):
|
|
|
66
66
|
self.check(self.RFC3986_BASE, "http:g", "http:g", True)
|
|
67
67
|
self.check(self.RFC3986_BASE, "http:g", "http://a/b/c/g", False)
|
|
68
68
|
|
|
69
|
+
def test_scheme_case(self):
|
|
70
|
+
# RFC 3986 3.1: scheme names are case-insensitive
|
|
71
|
+
self.check(self.RFC3986_BASE, "HTTP:g", "http://a/b/c/g")
|
|
72
|
+
self.check(self.RFC3986_BASE, "HTTP:g", "HTTP:g", strict=True)
|
|
73
|
+
self.check("HTTP://a/b/c/d;p?q", "http:g", "HTTP://a/b/c/g")
|
|
74
|
+
self.check("HTTP://a/b/c/d;p?q", "http:g", "http:g", strict=True)
|
|
75
|
+
self.check("g:h", "G:h", "g:h")
|
|
76
|
+
|
|
69
77
|
def test_rfc3986_merge(self):
|
|
70
78
|
"""urijoin test cases for RFC 3986 5.2.3. Merge Paths"""
|
|
71
79
|
self.check("http://a", "b", "http://a/b")
|
|
@@ -48,6 +48,8 @@ class SplitTest(unittest.TestCase):
|
|
|
48
48
|
("//?", (None, "", "", "", None)),
|
|
49
49
|
("//#", (None, "", "", None, "")),
|
|
50
50
|
("//?#", (None, "", "", "", "")),
|
|
51
|
+
("#foo\nbar", (None, None, "", None, "foo\nbar")),
|
|
52
|
+
("?foo\nbar#baz\n", (None, None, "", "foo\nbar", "baz\n")),
|
|
51
53
|
("?", (None, None, "", "", None)),
|
|
52
54
|
("??", (None, None, "", "?", None)),
|
|
53
55
|
("?#", (None, None, "", "", "")),
|
|
@@ -77,6 +79,8 @@ class SplitTest(unittest.TestCase):
|
|
|
77
79
|
(b"?:", (None, None, b"", b":", None)),
|
|
78
80
|
(b"#:", (None, None, b"", None, b":")),
|
|
79
81
|
(b"_:", (None, None, b"_:", None, None)),
|
|
82
|
+
(b"#foo\nbar", (None, None, b"", None, b"foo\nbar")),
|
|
83
|
+
(b"?foo\nbar#baz\n", (None, None, b"", b"foo\nbar", b"baz\n")),
|
|
80
84
|
]
|
|
81
85
|
for uri, parts in cases:
|
|
82
86
|
self.check(uri, parts)
|
|
@@ -281,9 +285,31 @@ class SplitTest(unittest.TestCase):
|
|
|
281
285
|
("nobody", "localhost", 42),
|
|
282
286
|
("userinfo", "test.python.org", 5432),
|
|
283
287
|
),
|
|
288
|
+
(
|
|
289
|
+
"urn:example:animal:ferret:nose",
|
|
290
|
+
(b"nobody", b"localhost", 42),
|
|
291
|
+
(b"nobody", b"localhost", 42),
|
|
292
|
+
),
|
|
293
|
+
("file:///", (b"nobody", b"localhost", 42), (b"nobody", b"localhost", 42)),
|
|
294
|
+
(
|
|
295
|
+
"http://Test.python.org/foo/",
|
|
296
|
+
(b"nobody", b"localhost", 42),
|
|
297
|
+
(b"nobody", "test.python.org", 42),
|
|
298
|
+
),
|
|
299
|
+
(
|
|
300
|
+
"http://@Test.python.org:5432/foo/",
|
|
301
|
+
(b"nobody", b"localhost", 42),
|
|
302
|
+
("", "test.python.org", 5432),
|
|
303
|
+
),
|
|
304
|
+
(
|
|
305
|
+
"http://userinfo@Test.python.org:5432/foo/",
|
|
306
|
+
(b"nobody", b"localhost", 42),
|
|
307
|
+
("userinfo", "test.python.org", 5432),
|
|
308
|
+
),
|
|
284
309
|
]
|
|
285
310
|
for uri, default, authority in cases:
|
|
286
311
|
self.assertEqual(urisplit(uri).getauthority(default), authority)
|
|
312
|
+
self.assertEqual(urisplit(uri.encode()).getauthority(default), authority)
|
|
287
313
|
for uri in ["http://[::1/", "http://::1]/"]:
|
|
288
314
|
with self.assertRaises(ValueError, msg="%r" % uri):
|
|
289
315
|
urisplit(uri).getauthority()
|
|
@@ -314,7 +340,7 @@ class SplitTest(unittest.TestCase):
|
|
|
314
340
|
def test_getport(self):
|
|
315
341
|
for uri in ["foo://bar", "foo://bar:", "foo://bar/", "foo://bar:/"]:
|
|
316
342
|
result = urisplit(uri)
|
|
317
|
-
if result.authority.endswith(":"):
|
|
343
|
+
if result.authority.endswith(":"): # type: ignore
|
|
318
344
|
self.assertEqual(result.port, "")
|
|
319
345
|
else:
|
|
320
346
|
self.assertEqual(result.port, None)
|
|
@@ -565,6 +591,12 @@ class SplitTest(unittest.TestCase):
|
|
|
565
591
|
urisplit(uri).gethost()
|
|
566
592
|
with self.assertRaises(ValueError, msg="%r" % uri.encode("ascii")):
|
|
567
593
|
urisplit(uri.encode("ascii")).gethost()
|
|
594
|
+
# RFC 3986 3.2.2: the IPvFuture version flag is case-insensitive
|
|
595
|
+
for uri in ["http://[v7.future]/", "http://[V7.future]/"]:
|
|
596
|
+
with self.assertRaisesRegex(ValueError, "mechanism not supported"):
|
|
597
|
+
urisplit(uri).gethost()
|
|
598
|
+
with self.assertRaisesRegex(ValueError, "mechanism not supported"):
|
|
599
|
+
urisplit(uri.encode("ascii")).gethost()
|
|
568
600
|
|
|
569
601
|
def test_encoding_none(self):
|
|
570
602
|
uri = "foo://user@example.com:8042/over/there?name=ferret#nose"
|
|
@@ -30,3 +30,17 @@ class UnsplitTest(unittest.TestCase):
|
|
|
30
30
|
]
|
|
31
31
|
for parts, uri in cases:
|
|
32
32
|
self.check(parts, uri)
|
|
33
|
+
|
|
34
|
+
def test_mixed_types(self):
|
|
35
|
+
# the type of `path` selects the result type; all other parts must match
|
|
36
|
+
cases = [
|
|
37
|
+
("foo", None, b"/over/there", None, None),
|
|
38
|
+
(b"foo", None, "/over/there", None, None),
|
|
39
|
+
(None, "example.com", b"/over/there", None, None),
|
|
40
|
+
(None, b"example.com", "/over/there", None, None),
|
|
41
|
+
(None, None, "/over/there", None, b"nose"),
|
|
42
|
+
(None, None, "/over/there", b"name=ferret", None),
|
|
43
|
+
]
|
|
44
|
+
for parts in cases:
|
|
45
|
+
with self.assertRaises(TypeError):
|
|
46
|
+
uriunsplit(parts)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[tox]
|
|
2
|
-
envlist = py,docs,doctest,ruff,ruff-format
|
|
2
|
+
envlist = py,docs,doctest,pyright,ruff,ruff-format
|
|
3
3
|
|
|
4
4
|
[testenv]
|
|
5
5
|
deps =
|
|
6
6
|
pytest
|
|
7
7
|
pytest-cov
|
|
8
8
|
commands =
|
|
9
|
-
py.test --basetemp={envtmpdir} --cov=uritools --cov-report term-missing {posargs}
|
|
9
|
+
py.test --basetemp={envtmpdir} --cov=uritools --cov-branch --cov-report term-missing {posargs}
|
|
10
10
|
|
|
11
11
|
[testenv:docs]
|
|
12
12
|
deps =
|
|
@@ -20,23 +20,20 @@ deps =
|
|
|
20
20
|
commands =
|
|
21
21
|
sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs {envtmpdir}/doctest
|
|
22
22
|
|
|
23
|
+
[testenv:pyright]
|
|
24
|
+
deps =
|
|
25
|
+
pyright
|
|
26
|
+
commands =
|
|
27
|
+
pyright {posargs}
|
|
28
|
+
|
|
23
29
|
[testenv:ruff]
|
|
24
30
|
deps =
|
|
25
31
|
ruff
|
|
26
32
|
commands =
|
|
27
33
|
ruff check {posargs}
|
|
28
|
-
skip_install = true
|
|
29
34
|
|
|
30
35
|
[testenv:ruff-format]
|
|
31
36
|
deps =
|
|
32
37
|
ruff
|
|
33
38
|
commands =
|
|
34
39
|
ruff format --diff -q {posargs}
|
|
35
|
-
skip_install = true
|
|
36
|
-
|
|
37
|
-
[testenv:pyright]
|
|
38
|
-
deps =
|
|
39
|
-
pyright
|
|
40
|
-
commands =
|
|
41
|
-
pyright {posargs}
|
|
42
|
-
skip_install = true
|
uritools-6.1.2/docs/conf.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
src_directory = (pathlib.Path(__file__).parent.parent / "src").resolve()
|
|
5
|
-
sys.path.insert(0, str(src_directory))
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# Extract the current version from the source.
|
|
9
|
-
def get_version():
|
|
10
|
-
"""Get the version and release from the source code."""
|
|
11
|
-
|
|
12
|
-
text = (src_directory / "uritools/__init__.py").read_text()
|
|
13
|
-
for line in text.splitlines():
|
|
14
|
-
if not line.strip().startswith("__version__"):
|
|
15
|
-
continue
|
|
16
|
-
full_version = line.partition("=")[2].strip().strip("\"'")
|
|
17
|
-
partial_version = ".".join(full_version.split(".")[:2])
|
|
18
|
-
return full_version, partial_version
|
|
19
|
-
return (None, None)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
project = "uritools"
|
|
23
|
-
copyright = "2014-2026 Thomas Kemmer"
|
|
24
|
-
release, version = get_version()
|
|
25
|
-
|
|
26
|
-
extensions = [
|
|
27
|
-
"sphinx.ext.autodoc",
|
|
28
|
-
"sphinx.ext.coverage",
|
|
29
|
-
"sphinx.ext.doctest",
|
|
30
|
-
"sphinx.ext.intersphinx",
|
|
31
|
-
"sphinx.ext.todo",
|
|
32
|
-
]
|
|
33
|
-
exclude_patterns = ["_build"]
|
|
34
|
-
master_doc = "index"
|
|
35
|
-
html_theme = "classic"
|
|
36
|
-
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|