Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-uritools for openSUSE:Factory
checked in at 2026-05-20 15:24:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-uritools (Old)
and /work/SRC/openSUSE:Factory/.python-uritools.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-uritools"
Wed May 20 15:24:24 2026 rev:8 rq:1354095 version:6.1.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-uritools/python-uritools.changes
2026-04-26 21:14:12.656524718 +0200
+++
/work/SRC/openSUSE:Factory/.python-uritools.new.1966/python-uritools.changes
2026-05-20 15:25:16.163989018 +0200
@@ -1,0 +2,9 @@
+Tue May 19 20:38:51 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 6.1.1:
+ * Minor documentation improvements.
+ * Update build environment.
+ * Add type stubs.
+ * Improve unit tests.
+
+-------------------------------------------------------------------
Old:
----
uritools-6.0.2.tar.gz
New:
----
uritools-6.1.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-uritools.spec ++++++
--- /var/tmp/diff_new_pack.VDC6UL/_old 2026-05-20 15:25:17.188031214 +0200
+++ /var/tmp/diff_new_pack.VDC6UL/_new 2026-05-20 15:25:17.188031214 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-uritools
-Version: 6.0.2
+Version: 6.1.1
Release: 0
Summary: URI parsing, classification and composition
License: MIT
@@ -26,7 +26,8 @@
URL: https://github.com/tkem/uritools/
Source:
https://files.pythonhosted.org/packages/source/u/uritools/uritools-%{version}.tar.gz
BuildRequires: %{python_module pip}
-BuildRequires: %{python_module setuptools}
+BuildRequires: %{python_module setuptools >= 78}
+BuildRequires: %{python_module setuptools-scm >= 8.2}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
++++++ uritools-6.0.2.tar.gz -> uritools-6.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/FUNDING.yml
new/uritools-6.1.1/.github/FUNDING.yml
--- old/uritools-6.0.2/.github/FUNDING.yml 1970-01-01 01:00:00.000000000
+0100
+++ new/uritools-6.1.1/.github/FUNDING.yml 2026-05-09 21:51:08.000000000
+0200
@@ -0,0 +1,2 @@
+github: [tkem]
+custom: ["https://www.paypal.me/tkem"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/ISSUE_TEMPLATE/bug_report.md
new/uritools-6.1.1/.github/ISSUE_TEMPLATE/bug_report.md
--- old/uritools-6.0.2/.github/ISSUE_TEMPLATE/bug_report.md 1970-01-01
01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.github/ISSUE_TEMPLATE/bug_report.md 2026-05-09
21:51:08.000000000 +0200
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: tkem
+
+---
+
+Before reporting a bug, please make sure you have the latest `uritools`
version installed:
+```
+pip install --upgrade uritools
+```
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Expected result**
+A clear and concise description of what you expected to happen.
+
+**Actual result**
+A clear and concise description of what happened instead.
+
+**Reproduction steps**
+
+```python
+import uritools
+
+```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/ISSUE_TEMPLATE/config.yml
new/uritools-6.1.1/.github/ISSUE_TEMPLATE/config.yml
--- old/uritools-6.0.2/.github/ISSUE_TEMPLATE/config.yml 1970-01-01
01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.github/ISSUE_TEMPLATE/config.yml 2026-05-09
21:51:08.000000000 +0200
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/uritools-6.0.2/.github/ISSUE_TEMPLATE/feature_request.md
new/uritools-6.1.1/.github/ISSUE_TEMPLATE/feature_request.md
--- old/uritools-6.0.2/.github/ISSUE_TEMPLATE/feature_request.md
1970-01-01 01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.github/ISSUE_TEMPLATE/feature_request.md
2026-05-09 21:51:08.000000000 +0200
@@ -0,0 +1,10 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: tkem
+
+---
+
+Sorry, but `uritools` is not accepting feature requests at this time.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/SECURITY.md
new/uritools-6.1.1/.github/SECURITY.md
--- old/uritools-6.0.2/.github/SECURITY.md 1970-01-01 01:00:00.000000000
+0100
+++ new/uritools-6.1.1/.github/SECURITY.md 2026-05-09 21:51:08.000000000
+0200
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+Security updates are applied only to the latest release.
+
+## Reporting a Vulnerability
+
+If you have discovered a security vulnerability in this project, please report
it privately. **Do not disclose it as a public issue.** This gives us time to
work with you to fix the issue before public exposure, reducing the chance that
the exploit will be used before a patch is released.
+
+Please disclose it at [security
advisory](https://github.com/tkem/uritools/security/advisories/new).
+
+This project is maintained by a single person on a best effort basis. As such,
vulnerability reports will be investigated and fixed or disclosed as soon as
possible, but there may be delays in response time due to the maintainer's
other commitments.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/copilot-instructions.md
new/uritools-6.1.1/.github/copilot-instructions.md
--- old/uritools-6.0.2/.github/copilot-instructions.md 1970-01-01
01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.github/copilot-instructions.md 2026-05-18
20:08:33.000000000 +0200
@@ -0,0 +1,76 @@
+# uritools Project Guidelines
+
+## Overview
+
+RFC 3986 compliant URI parsing library replacing Python's `urllib.parse`.
Requires Python >= 3.10, zero runtime dependencies.
+
+- **Single-file design**: Everything in `src/uritools/__init__.py` — do not
create submodules
+- **Dual type support**: All public functions handle both `str` and `bytes`
via `SplitResultString` / `SplitResultBytes`
+- **Public API**: Defined in `__all__` — changes are breaking
+- **Type stubs**: `src/uritools/__init__.pyi` — keep in sync with runtime API
+
+## Architecture
+
+Functions return `namedtuple` subclasses (`SplitResult`, `DefragResult`) with
both tuple access and getter methods (`.gethost()`, `.getport()`,
`.getquerydict()`). Preserve both interfaces when modifying.
+
+Encoding uses cached lookup tables (`_encoded` dict) with uppercase hex digits
per RFC 3986. Safe characters vary by component (`_SAFE_PATH`, `_SAFE_QUERY`,
etc.).
+
+Type dispatch pattern used throughout:
+```python
+if isinstance(uristring, bytes):
+ result = SplitResultBytes
+else:
+ result = SplitResultString
+```
+
+### Composition rules (`uricompose()`)
+- Scheme: `[A-Za-z][A-Za-z0-9+.-]*`, lowercased
+- Path with authority must start with `/` or be empty
+- Path without authority cannot start with `//`
+- Relative paths with `:` in first segment get `./` prefix
+- Query accepts `str`, `bytes`, `dict`, or list of tuples
+- Invalid host types must raise `TypeError`, not leak `AttributeError`
+
+### IP address handling
+- Use `ipaddress` module; reject IPvFuture (addresses starting with `v`)
+- IPv6 bracketed in output: `[2001:db8::1]`, always compressed form
+
+### Design decisions (intentional — do not "fix")
+- `getscheme()` always returns `str` (decodes bytes schemes)
+- `getfragment()` and other `get*()` methods decode to `str` by default, even
for bytes input
+- `gethost()` returns `""` for empty host when `default=None`, but returns
`default` when it's not None
+- `SplitResultBytes` / `SplitResultString` are semi-private (not in `__all__`)
+
+## Development
+
+### Running checks
+```bash
+tox # all environments (recommended)
+tox -e py # tests with coverage
+tox -e ruff-format # formatting (ruff format)
+tox -e ruff # linting (ruff check)
+tox -e pyright # type checking
+tox -e docs # Sphinx HTML build
+tox -e doctest # Sphinx doctest
+```
+
+Use `tox` or `PYTHONPATH=src python3 -m pytest tests/` to test against the
workspace source (not the system-installed package).
+
+### Testing conventions
+- `unittest.TestCase` with `check()` helpers — not pytest-style
+- Test both `str` and `bytes` variants of every case
+- Test RFC 3986 examples explicitly in `test_rfc3986()` methods
+- Edge cases in separate `test_abnormal()` methods
+- Maintain 100% line coverage
+- Avoid shadowing built-ins (`dict`, `input`) in loop variables
+
+### Documentation
+- Sphinx docs in `docs/`, built with `tox -e docs`
+- `README.rst` examples must work as doctests
+- Docstrings describe types in prose (no type annotations in signatures)
+- Directive body content uses 3-space indentation in `docs/index.rst`
+
+### Changelog
+- `CHANGELOG.rst` uses `vX.Y.Z (YYYY-MM-DD)` headers with `===` underlines
+- Unreleased changes go under `vX.Y.Z (UNRELEASED)`
+- Each entry is a `- ` bullet (imperative mood, no trailing period)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/copilot-review.md
new/uritools-6.1.1/.github/copilot-review.md
--- old/uritools-6.0.2/.github/copilot-review.md 1970-01-01
01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.github/copilot-review.md 2026-05-18
20:08:33.000000000 +0200
@@ -0,0 +1,158 @@
+# Code, Tests & Docs Review
+
+**Date:** 2026-05-18
+**Version:** 6.1.1
+**Python:** 3.13.7
+**Scope:** `src/uritools/__init__.py`, `src/uritools/__init__.pyi`, `tests/`,
`docs/`, `README.rst`
+
+## Checks Summary
+
+| Check | Result |
+|------------|--------|
+| Tests | 45 passed |
+| Coverage | 100% (394 statements) |
+| Lint | Clean (ruff check) |
+| Format | Clean (ruff format) |
+| Type check | 0 errors (pyright) |
+| Docs build | OK (sphinx html) |
+| Doctests | 10 passed |
+
+---
+
+## Code Findings
+
+### C1. Unbounded cache growth in `uriencode()` (low risk)
+
+`_encoded` dict grows without limit when callers pass arbitrary `safe` values.
The existing FIXME (line 79) acknowledges this. Consider `@functools.lru_cache`
or capping the dict size. In practice, the library only uses a handful of fixed
`safe` values internally, so risk is low unless users call `uriencode()`
directly with dynamic `safe` arguments.
+
+### C2. `DefragResult.getfragment()` always returns `str` (design note)
+
+The FIXME at line 129 notes that `getfragment()` decodes to `str` by default
even when `geturi()` returns `bytes`. This is consistent with
`SplitResult.getfragment()` and all other `get*()` methods, so changing it
would be a breaking API change. The FIXME can be considered resolved or
intentional.
+
+### C3. `getscheme()` always returns `str` (design note)
+
+Similar FIXME at line 203: `getscheme()` decodes bytes schemes to `str`. The
stub file already types this as `-> str | None`, confirming this is intentional
behavior.
+
+### C4. `SplitResultBytes` / `SplitResultString` visibility (minor)
+
+Both classes have a `# TODO: make private?` comment. They are not in `__all__`
so they are already semi-private. Making them fully private (prefixing with
`_`) would be a minor cleanup but could break users who import them directly.
+
+### C5. `gethost()` returns empty string for empty host
+
+`gethost(default=X)` returns `default` when host is empty *and* default is not
None, but returns `""` when default is None. This edge case (`"file:///"` ->
`gethost()` returns `""`) is tested and documented, but the asymmetry between
`default=None` and `default="x"` may be unexpected.
+
+### C6. `_AUTHORITY_RE_*` port group matches empty string
+
+The regex `(?::([0-9]*))?$` captures an empty port string for URIs like
`http://host:`. This is handled correctly downstream (`_port()` returns `b""`
for empty port, `getport()` returns default), but the empty-string-vs-None
distinction for port is a subtle edge.
+
+### C8. `getauthority()` TODO about efficiency (trivial)
+
+Line 224: `# TODO: this could be much more efficient by using a dedicated
regex` — `getauthority()` calls three separate getters. Could be
micro-optimized with a single regex, but unlikely to matter in practice.
+
+---
+
+## Test Findings
+
+### Good practices observed
+- All public functions tested with both `str` and `bytes` input
+- RFC 3986 examples tested exhaustively (Sections 3, 5.4.1, 5.4.2)
+- Edge cases and abnormal inputs covered thoroughly
+- `check()` helpers enforce round-trip consistency (parse → recompose)
+- IPv4, IPv6, IPv4-mapped IPv6, and IPvFuture rejection all tested
+- Error cases test `TypeError` vs `ValueError` distinction
+- Cross-type `urijoin` (bytes base + str ref, etc.) tested
+- 100% line coverage maintained
+- Python 3.13 IPv4-mapped address representation change handled in
`test_ipv4_mapped_literal` with dual expected values
+
+### T1. `uricompose` with `fragment` parameter
+
+Only tested implicitly via the RFC 3986 example in `test_rfc3986`. No
dedicated test for fragment encoding (e.g., `fragment="foo bar"` producing
`#foo%20bar`).
+
+### T2. `uricompose` with non-default `encoding`
+
+No test passes a non-default `encoding` to `uricompose()`. For example,
composing with `encoding="latin-1"` and non-ASCII path characters.
+
+### T3. `uricompose` authority override with bytes
+
+`test_authority_override` only uses `str` values. No test overrides authority
subcomponents with `bytes` kwargs.
+
+### T4. `uriunsplit` with mixed types
+
+The type dispatch in `uriunsplit()` uses `path` type to select the result
class. Mixed types (e.g., str scheme + bytes path) could produce unexpected
results but are not tested. Consider documenting this as unsupported or adding
a guard.
+
+### T5. `DefragResult.getfragment()` default parameter
+
+No test passes a non-None `default` to `DefragResult.getfragment()` (e.g.,
`uridefrag("").getfragment(default="fallback")`).
+
+### T6. `getauthority` with bytes default tuple
+
+`test_encoding_none` tests str and bytes URI input, but `getauthority()` with
a bytes default tuple is not tested.
+
+---
+
+## Documentation Findings
+
+### D3. `docs/index.rst`: no documentation for `uriencode`/`uridecode`
parameters
+
+The `uriencode` and `uridecode` docs describe input/output types but do not
document the `encoding`, `errors`, or `safe` parameters individually. Users
must read the function signature and docstring. Consider adding parameter
descriptions.
+
+### D4. `docs/index.rst`: `SplitResult` attribute table missing `Index` for
properties
+
+The `urisplit` attribute table has empty `Index` cells for `userinfo`, `host`,
and `port`. This is intentional (they are properties, not tuple fields), but it
could be clearer with a note like "n/a" or "(property)".
+
+### D6. `docs/conf.py`: `master_doc` deprecated
+
+`master_doc` is deprecated in Sphinx 4.0+ in favor of `root_doc`. Works fine
currently but will eventually produce a warning.
+
+### D7. `docs/index.rst`: `DefragResult` and `SplitResult` documented twice
+
+These classes are first described in the "URI Decomposition" section (inline
attribute tables) and then again in "Structured Parse Results" via `..
autoclass::` with `:members:`. This provides the best of both worlds (table
overview + detailed API), but the duplication could confuse readers scanning
for the complete API of these classes.
+
+### D8. Changelog entry format guidance
+
+The `CHANGELOG.rst` format is documented in `copilot-instructions.md`
(imperative mood bullets under `vX.Y.Z (YYYY-MM-DD)` headers with `===`
underlines). No separate `CONTRIBUTING.md` exists, but the conventions are
clear from the instructions file.
+
+---
+
+## Type Stub Findings
+
+### S1. `DefragResult` and `SplitResult` stub inheritance
+
+The stubs declare `DefragResult(NamedTuple, Generic[AnyStr])` and
`SplitResult(NamedTuple, Generic[AnyStr])`, but the runtime classes inherit
from `collections.namedtuple(...)` (not `typing.NamedTuple`). This is
acceptable for type checking purposes since pyright accepts it (0 errors), but
it's a divergence from the runtime class hierarchy.
+
+### S2. `gethost()` default parameter type
+
+The stub types `default` as `str | ipaddress.IPv4Address |
ipaddress.IPv6Address | None`, but at runtime any value could be passed as
default (it's just returned as-is when host is absent). This is appropriate —
the stub reflects intended usage, not arbitrary usage.
+
+### S3. `uriencode` overload for str input missing `encoding=None`
+
+The str overload of `uriencode` types `encoding` as `str`, not `str | None`.
The bytes overload allows `str | None`. At runtime, passing `encoding=None` to
`uriencode` with a str input would fail (can't call `str.encode(None)`), so the
stub is correct — but this asymmetry is not obvious.
+
+---
+
+## Security
+
+No security issues found. The library:
+- Does not perform network I/O
+- Does not execute or evaluate URI content
+- Properly validates IP literals and rejects IPvFuture
+- Uses percent-encoding with uppercase hex digits per RFC 3986
+- Handles path traversal (`../`) correctly per RFC 3986 Section 5.2.4
+- `test_path_traversal_limits` confirms `../` * 100 cannot escape root
+
+---
+
+## Summary
+
+The codebase is clean, well-tested (100% coverage), and fully passing all
checks. Key findings by priority:
+
+**Consider addressing:**
+1. **C1**: Consider capping `_encoded` cache or switching to `lru_cache`
+
+**Nice to have:**
+2. **T1-T3**: Add test cases for `uricompose` fragment, encoding, and bytes
authority override
+3. **C2-C3**: Resolve or remove FIXME comments about bytes return types
(behavior is intentional)
+4. **D6**: Rename `master_doc` to `root_doc` in `docs/conf.py`
+
+**No action needed:**
+- C4, C5, C6, C8, D3, D4, D7, D8, S1-S3, T4-T6 — documented here for awareness
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/dependabot.yml
new/uritools-6.1.1/.github/dependabot.yml
--- old/uritools-6.0.2/.github/dependabot.yml 1970-01-01 01:00:00.000000000
+0100
+++ new/uritools-6.1.1/.github/dependabot.yml 2026-05-09 21:51:08.000000000
+0200
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.github/workflows/ci.yml
new/uritools-6.1.1/.github/workflows/ci.yml
--- old/uritools-6.0.2/.github/workflows/ci.yml 1970-01-01 01:00:00.000000000
+0100
+++ new/uritools-6.1.1/.github/workflows/ci.yml 2026-05-09 21:51:08.000000000
+0200
@@ -0,0 +1,27 @@
+name: CI
+
+on: [push, pull_request, workflow_dispatch]
+
+permissions:
+ contents: read
+
+jobs:
+ main:
+ name: Python ${{ matrix.python }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ["3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t",
"pypy3.10", "pypy3.11"]
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #
v6.0.2
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #
v6.2.0
+ with:
+ python-version: ${{ matrix.python }}
+ allow-prereleases: true
+ - run: pip install coverage tox
+ - run: tox
+ - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
# v6.0.0
+ with:
+ name: ${{ matrix.python }}
+ token: ${{ secrets.CODECOV_TOKEN }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.gitignore
new/uritools-6.1.1/.gitignore
--- old/uritools-6.0.2/.gitignore 1970-01-01 01:00:00.000000000 +0100
+++ new/uritools-6.1.1/.gitignore 2026-05-09 21:51:08.000000000 +0200
@@ -0,0 +1,11 @@
+*.egg-info
+*.pyc
+*.swp
+.cache/
+.coverage
+.pytest_cache/
+.tox/
+MANIFEST
+build/
+dist/
+docs/_build/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/.readthedocs.yaml
new/uritools-6.1.1/.readthedocs.yaml
--- old/uritools-6.0.2/.readthedocs.yaml 1970-01-01 01:00:00.000000000
+0100
+++ new/uritools-6.1.1/.readthedocs.yaml 2026-05-09 21:51:08.000000000
+0200
@@ -0,0 +1,11 @@
+# Configure ReadTheDocs.
+
+version: 2
+
+build:
+ os: "ubuntu-22.04"
+ tools:
+ python: "3.11"
+
+sphinx:
+ configuration: "docs/conf.py"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/CHANGELOG.rst
new/uritools-6.1.1/CHANGELOG.rst
--- old/uritools-6.0.2/CHANGELOG.rst 2026-04-22 22:09:19.000000000 +0200
+++ new/uritools-6.1.1/CHANGELOG.rst 2026-05-18 20:08:33.000000000 +0200
@@ -1,3 +1,19 @@
+v6.1.1 (2026-05-18)
+===================
+
+- Minor documentation improvements.
+
+- Update build environment.
+
+
+v6.1.0 (2026-04-30)
+===================
+
+- Add type stubs.
+
+- Improve unit tests.
+
+
v6.0.2 (2026-04-22)
===================
@@ -7,6 +23,7 @@
- Update CI environment.
+
v6.0.1 (2025-12-21)
===================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/LICENSE new/uritools-6.1.1/LICENSE
--- old/uritools-6.0.2/LICENSE 2025-05-02 15:05:47.000000000 +0200
+++ new/uritools-6.1.1/LICENSE 2026-05-18 20:08:33.000000000 +0200
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014-2025 Thomas Kemmer
+Copyright (c) 2014-2026 Thomas Kemmer
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/MANIFEST.in
new/uritools-6.1.1/MANIFEST.in
--- old/uritools-6.0.2/MANIFEST.in 2024-05-01 20:41:31.000000000 +0200
+++ new/uritools-6.1.1/MANIFEST.in 1970-01-01 01:00:00.000000000 +0100
@@ -1,11 +0,0 @@
-include CHANGELOG.rst
-include LICENSE
-include MANIFEST.in
-include README.rst
-include tox.ini
-exclude .readthedocs.yaml
-
-recursive-include docs *
-prune docs/_build
-
-recursive-include tests *.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/PKG-INFO new/uritools-6.1.1/PKG-INFO
--- old/uritools-6.0.2/PKG-INFO 2026-04-22 22:16:59.532577000 +0200
+++ new/uritools-6.1.1/PKG-INFO 2026-05-18 20:22:32.359385700 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: uritools
-Version: 6.0.2
+Version: 6.1.1
Summary: URI parsing, classification and composition
Author-email: Thomas Kemmer <[email protected]>
Maintainer-email: Thomas Kemmer <[email protected]>
@@ -46,19 +46,10 @@
:target: https://codecov.io/gh/tkem/uritools
:alt: Test coverage
-.. image:: https://img.shields.io/librariesio/sourcerank/pypi/uritools
- :target: https://libraries.io/pypi/uritools
- :alt: Libraries.io SourceRank
-
.. image:: https://img.shields.io/github/license/tkem/uritools
:target: https://raw.github.com/tkem/uritools/master/LICENSE
:alt: License
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/psf/black
- :alt: Code style: black
-
-
This module provides RFC 3986 compliant functions for parsing,
classifying and composing URIs and URI references, largely replacing
the Python Standard Library's ``urllib.parse`` module.
@@ -86,10 +77,9 @@
>>> urijoin(uriunsplit(parts), '/right/here?name=swallow#beak')
'foo://example.com:8042/right/here?name=swallow#beak'
-For various reasons, ``urllib.parse`` and its Python 2 predecessor
-``urlparse`` are not compliant with current Internet standards. As
-stated in `Lib/urllib/parse.py
-<https://github.com/python/cpython/blob/main/Lib/urllib/parse.py#L22>`_:
+For various reasons, ``urllib.parse`` is not compliant with current
+Internet standards. As stated in `Lib/urllib/parse.py
+<https://github.com/python/cpython/blob/3.14/Lib/urllib/parse.py#L22>`_:
RFC 3986 is considered the current standard and any future changes
to urlparse module should conform with it. The urlparse module is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/README.rst
new/uritools-6.1.1/README.rst
--- old/uritools-6.0.2/README.rst 2026-04-22 22:09:19.000000000 +0200
+++ new/uritools-6.1.1/README.rst 2026-05-18 20:08:33.000000000 +0200
@@ -17,19 +17,10 @@
:target: https://codecov.io/gh/tkem/uritools
:alt: Test coverage
-.. image:: https://img.shields.io/librariesio/sourcerank/pypi/uritools
- :target: https://libraries.io/pypi/uritools
- :alt: Libraries.io SourceRank
-
.. image:: https://img.shields.io/github/license/tkem/uritools
:target: https://raw.github.com/tkem/uritools/master/LICENSE
:alt: License
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/psf/black
- :alt: Code style: black
-
-
This module provides RFC 3986 compliant functions for parsing,
classifying and composing URIs and URI references, largely replacing
the Python Standard Library's ``urllib.parse`` module.
@@ -57,10 +48,9 @@
>>> urijoin(uriunsplit(parts), '/right/here?name=swallow#beak')
'foo://example.com:8042/right/here?name=swallow#beak'
-For various reasons, ``urllib.parse`` and its Python 2 predecessor
-``urlparse`` are not compliant with current Internet standards. As
-stated in `Lib/urllib/parse.py
-<https://github.com/python/cpython/blob/main/Lib/urllib/parse.py#L22>`_:
+For various reasons, ``urllib.parse`` is not compliant with current
+Internet standards. As stated in `Lib/urllib/parse.py
+<https://github.com/python/cpython/blob/3.14/Lib/urllib/parse.py#L22>`_:
RFC 3986 is considered the current standard and any future changes
to urlparse module should conform with it. The urlparse module is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/docs/.gitignore
new/uritools-6.1.1/docs/.gitignore
--- old/uritools-6.0.2/docs/.gitignore 2020-08-10 19:35:07.000000000 +0200
+++ new/uritools-6.1.1/docs/.gitignore 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-_build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/docs/conf.py
new/uritools-6.1.1/docs/conf.py
--- old/uritools-6.0.2/docs/conf.py 2026-04-22 22:09:19.000000000 +0200
+++ new/uritools-6.1.1/docs/conf.py 2026-05-09 21:51:08.000000000 +0200
@@ -16,6 +16,7 @@
full_version = line.partition("=")[2].strip().strip("\"'")
partial_version = ".".join(full_version.split(".")[:2])
return full_version, partial_version
+ return (None, None)
project = "uritools"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/docs/index.rst
new/uritools-6.1.1/docs/index.rst
--- old/uritools-6.0.2/docs/index.rst 2025-05-02 15:51:45.000000000 +0200
+++ new/uritools-6.1.1/docs/index.rst 2026-05-18 20:08:33.000000000 +0200
@@ -31,10 +31,9 @@
>>> urijoin(uriunsplit(parts), '/right/here?name=swallow#beak')
'foo://example.com:8042/right/here?name=swallow#beak'
-For various reasons, :mod:`urllib.parse` and its Python 2 predecessor
-:mod:`urlparse` are not compliant with current Internet standards. As
-stated in `Lib/urllib/parse.py
-<https://github.com/python/cpython/blob/main/Lib/urllib/parse.py#L22>`_:
+For various reasons, :mod:`urllib.parse` is not compliant with current
+Internet standards. As stated in `Lib/urllib/parse.py
+<https://github.com/python/cpython/blob/3.14/Lib/urllib/parse.py#L22>`_:
RFC 3986 is considered the current standard and any future changes
to urlparse module should conform with it. The urlparse module is
@@ -103,7 +102,7 @@
All components may be specified as either Unicode strings, which
will be encoded according to `encoding`, or :class:`bytes` objects.
- `authority` may also be passed a three-item iterable specifying
+ `authority` may also be passed a three-item sequence specifying
userinfo, host and port subcomponents. If both `authority` and any
of the `userinfo`, `host` or `port` keyword arguments are given,
the keyword argument will override the corresponding `authority`
@@ -117,8 +116,8 @@
.. autofunction:: urijoin
- If `strict` is :const:`False`, a scheme in the reference is
- ignored if it is identical to the base URI's scheme.
+ If `strict` is :const:`False`, a scheme in the reference is
+ ignored if it is identical to the base URI's scheme.
.. autofunction:: uriunsplit
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/pyproject.toml
new/uritools-6.1.1/pyproject.toml
--- old/uritools-6.0.2/pyproject.toml 2026-04-22 22:09:19.000000000 +0200
+++ new/uritools-6.1.1/pyproject.toml 2026-05-18 20:08:33.000000000 +0200
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools >= 61.0.0", "wheel"]
+requires = ["setuptools >= 78", "setuptools-scm >= 8.2"]
build-backend = "setuptools.build_meta"
[project]
@@ -47,10 +47,7 @@
[tool.setuptools.dynamic]
version = {attr = "uritools.__version__"}
-[tool.flake8]
-max-line-length = 80
-exclude = [".git", ".tox", "build"]
-select = ["C", "E", "F", "W", "B", "B950", "I", "N"]
-# E501: line too long (black)
-# W503 line break before binary operator (black)
-ignore = ["E501", "W503"]
+[tool.pyright]
+typeCheckingMode = "standard"
+reportOptionalMemberAccess = false
+reportAttributeAccessIssue = false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/src/uritools/__init__.py
new/uritools-6.1.1/src/uritools/__init__.py
--- old/uritools-6.0.2/src/uritools/__init__.py 2026-04-22 22:09:19.000000000
+0200
+++ new/uritools-6.1.1/src/uritools/__init__.py 2026-05-18 20:08:33.000000000
+0200
@@ -33,7 +33,7 @@
"uriunsplit",
)
-__version__ = "6.0.2"
+__version__ = "6.1.1"
# RFC 3986 2.2. Reserved Characters
@@ -84,7 +84,7 @@
except KeyError:
encoded = _encoded[b""][:]
for i in safe:
- encoded[i] = bytes([i])
+ encoded[i] = bytes([i]) # type: ignore
_encoded[safe] = encoded
return b"".join(map(encoded.__getitem__, uristring))
@@ -126,6 +126,7 @@
original URI did not contain a fragment component.
"""
+ # FIXME: by default, getfragment() should return bytes if geturi()
returns bytes
fragment = self.fragment
if fragment is not None:
return uridecode(fragment, encoding, errors)
@@ -145,7 +146,7 @@
authority = self.authority
if authority is None:
return None
- userinfo, present, _ = authority.rpartition(self.AT)
+ userinfo, present, _ = authority.rpartition(self._AT)
if present:
return userinfo
else:
@@ -156,9 +157,9 @@
authority = self.authority
if authority is None:
return None
- _, _, hostinfo = authority.rpartition(self.AT)
- host, _, port = hostinfo.rpartition(self.COLON)
- if port.lstrip(self.DIGITS):
+ _, _, hostinfo = authority.rpartition(self._AT)
+ host, _, port = hostinfo.rpartition(self._COLON)
+ if port.lstrip(self._DIGITS):
return hostinfo
else:
return host
@@ -168,8 +169,8 @@
authority = self.authority
if authority is None:
return None
- _, present, port = authority.rpartition(self.COLON)
- if present and not port.lstrip(self.DIGITS):
+ _, present, port = authority.rpartition(self._COLON)
+ if present and not port.lstrip(self._DIGITS):
return port
else:
return None
@@ -184,21 +185,22 @@
# RFC 3986 5.3. Component Recomposition
result = []
if scheme is not None:
- result.extend([scheme, self.COLON])
+ result.extend([scheme, self._COLON])
if authority is not None:
- result.extend([self.SLASH, self.SLASH, authority])
+ result.extend([self._SLASH, self._SLASH, authority])
result.append(path)
if query is not None:
- result.extend([self.QUEST, query])
+ result.extend([self._QUEST, query])
if fragment is not None:
- result.extend([self.HASH, fragment])
- return self.EMPTY.join(result)
+ result.extend([self._HASH, fragment])
+ return self._EMPTY.join(result)
def getscheme(self, default=None):
"""Return the URI scheme in canonical (lowercase) form, or `default`
if the original URI reference did not contain a scheme component.
"""
+ # FIXME: should getscheme() return bytes if geturi() returns bytes?
scheme = self.scheme
if scheme is None:
return default
@@ -215,7 +217,7 @@
# TBD: (userinfo, host, port) kwargs, default string?
if default is None:
default = (None, None, None)
- elif not isinstance(default, collections.abc.Iterable):
+ elif not isinstance(default, collections.abc.Sequence):
raise TypeError("Invalid default type")
elif len(default) != 3:
raise ValueError("Invalid default length")
@@ -247,9 +249,9 @@
host = self.host
if host is None or (not host and default is not None):
return default
- elif host.startswith(self.LBRACKET) and host.endswith(self.RBRACKET):
+ elif host.startswith(self._LBRACKET) and host.endswith(self._RBRACKET):
return self.__parse_ip_literal(host[1:-1])
- elif host.startswith(self.LBRACKET) or host.endswith(self.RBRACKET):
+ elif host.startswith(self._LBRACKET) or host.endswith(self._RBRACKET):
raise ValueError("Invalid host %r: mismatched brackets" % host)
# TODO: faster check for IPv4 address?
try:
@@ -315,7 +317,7 @@
else:
qsl = self.query.split(sep.encode("ascii"))
result = []
- for parts in [qs.partition(self.EQ) for qs in qsl if qs]:
+ for parts in [qs.partition(self._EQ) for qs in qsl if qs]:
name = uridecode(parts[0], encoding, errors)
if parts[1]:
value = uridecode(parts[2], encoding, errors)
@@ -352,7 +354,7 @@
return (
self.scheme is None
and self.authority is None
- and self.path.startswith(self.SLASH)
+ and self.path.startswith(self._SLASH)
)
def isrelpath(self):
@@ -360,7 +362,7 @@
return (
self.scheme is None
and self.authority is None
- and not self.path.startswith(self.SLASH)
+ and not self.path.startswith(self._SLASH)
)
def issamedoc(self):
@@ -377,7 +379,7 @@
:class:`SplitResult` representing its target URI.
"""
- scheme, authority, path, query, fragment = self.RE.match(ref).groups()
+ scheme, authority, path, query, fragment = self._match(ref).groups()
# RFC 3986 5.2.2. Transform References
if scheme is not None and (strict or scheme != self.scheme):
@@ -390,7 +392,7 @@
authority = self.authority
path = self.path
query = self.query if query is None else query
- elif path.startswith(self.SLASH):
+ elif path.startswith(self._SLASH):
scheme = self.scheme
authority = self.authority
path = self.__remove_dot_segments(path)
@@ -403,32 +405,32 @@
def __merge(self, path):
# RFC 3986 5.2.3. Merge Paths
if self.authority is not None and not self.path:
- return self.SLASH + path
+ return self._SLASH + path
else:
- parts = self.path.rpartition(self.SLASH)
+ parts = self.path.rpartition(self._SLASH)
return parts[1].join((parts[0], path))
@classmethod
def __remove_dot_segments(cls, path):
# RFC 3986 5.2.4. Remove Dot Segments
pseg = []
- for s in path.split(cls.SLASH):
- if s == cls.DOT:
+ for s in path.split(cls._SLASH):
+ if s == cls._DOT:
continue
- elif s != cls.DOTDOT:
+ elif s != cls._DOTDOT:
pseg.append(s)
elif len(pseg) == 1 and not pseg[0]:
continue
- elif pseg and pseg[-1] != cls.DOTDOT:
+ elif pseg and pseg[-1] != cls._DOTDOT:
pseg.pop()
else:
pseg.append(s)
# adjust for trailing '/.' or '/..'
- if path.rpartition(cls.SLASH)[2] in (cls.DOT, cls.DOTDOT):
- pseg.append(cls.EMPTY)
- if path and len(pseg) == 1 and pseg[0] == cls.EMPTY:
- pseg.insert(0, cls.DOT)
- return cls.SLASH.join(pseg)
+ if path.rpartition(cls._SLASH)[2] in (cls._DOT, cls._DOTDOT):
+ pseg.append(cls._EMPTY)
+ if path and len(pseg) == 1 and pseg[0] == cls._EMPTY:
+ pseg.insert(0, cls._DOT)
+ return cls._SLASH.join(pseg)
@classmethod
def __parse_ip_literal(cls, address):
@@ -454,11 +456,12 @@
return ipaddress.IPv6Address(address)
+# TODO: make private?
class SplitResultBytes(SplitResult):
__slots__ = () # prevent creation of instance dictionary
# RFC 3986 Appendix B
- RE = re.compile(
+ _RE = re.compile(
rb"""
(?:([A-Za-z][A-Za-z0-9+.-]*):)? # scheme (RFC 3986 3.1)
(?://([^/?#]*))? # authority
@@ -469,8 +472,12 @@
flags=re.VERBOSE,
)
+ @classmethod
+ def _match(cls, ref):
+ return cls._RE.match(ref)
+
# RFC 3986 2.2 gen-delims
- COLON, SLASH, QUEST, HASH, LBRACKET, RBRACKET, AT = (
+ _COLON, _SLASH, _QUEST, _HASH, _LBRACKET, _RBRACKET, _AT = (
b":",
b"/",
b"?",
@@ -481,18 +488,19 @@
)
# RFC 3986 3.3 dot-segments
- DOT, DOTDOT = b".", b".."
+ _DOT, _DOTDOT = b".", b".."
- EMPTY, EQ = b"", b"="
+ _EMPTY, _EQ = b"", b"="
- DIGITS = b"0123456789"
+ _DIGITS = b"0123456789"
+# TODO: make private?
class SplitResultString(SplitResult):
__slots__ = () # prevent creation of instance dictionary
# RFC 3986 Appendix B
- RE = re.compile(
+ _RE = re.compile(
r"""
(?:([A-Za-z][A-Za-z0-9+.-]*):)? # scheme (RFC 3986 3.1)
(?://([^/?#]*))? # authority
@@ -503,8 +511,12 @@
flags=re.VERBOSE,
)
+ @classmethod
+ def _match(cls, ref):
+ return cls._RE.match(ref)
+
# RFC 3986 2.2 gen-delims
- COLON, SLASH, QUEST, HASH, LBRACKET, RBRACKET, AT = (
+ _COLON, _SLASH, _QUEST, _HASH, _LBRACKET, _RBRACKET, _AT = (
":",
"/",
"?",
@@ -515,11 +527,11 @@
)
# RFC 3986 3.3 dot-segments
- DOT, DOTDOT = ".", ".."
+ _DOT, _DOTDOT = ".", ".."
- EMPTY, EQ = "", "="
+ _EMPTY, _EQ = "", "="
- DIGITS = "0123456789"
+ _DIGITS = "0123456789"
def uridefrag(uristring):
@@ -542,7 +554,7 @@
result = SplitResultBytes
else:
result = SplitResultString
- return result(*result.RE.match(uristring).groups())
+ return result(*result._match(uristring).groups())
def uriunsplit(parts):
@@ -601,8 +613,6 @@
return urisplit(uristring).issamedoc()
-# TBD: move compose to its own submodule?
-
# RFC 3986 3.1: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
_SCHEME_RE = re.compile(b"^[A-Za-z][A-Za-z0-9+.-]*$")
@@ -741,14 +751,14 @@
elif scheme is not None:
scheme = _scheme(scheme.encode())
- # authority must be string type or three-item iterable
+ # authority must be string type or three-item sequence
if authority is None:
authority = (None, None, None)
elif isinstance(authority, bytes):
authority = _AUTHORITY_RE_BYTES.match(authority).groups()
elif isinstance(authority, str):
authority = _AUTHORITY_RE_STR.match(authority).groups()
- elif not isinstance(authority, collections.abc.Iterable):
+ elif not isinstance(authority, collections.abc.Sequence):
raise TypeError("Invalid authority type")
elif len(authority) != 3:
raise ValueError("Invalid authority length")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/src/uritools/__init__.pyi
new/uritools-6.1.1/src/uritools/__init__.pyi
--- old/uritools-6.0.2/src/uritools/__init__.pyi 1970-01-01
01:00:00.000000000 +0100
+++ new/uritools-6.1.1/src/uritools/__init__.pyi 2026-05-09
21:51:08.000000000 +0200
@@ -0,0 +1,243 @@
+import ipaddress
+from collections.abc import Iterable, Mapping, Sequence
+from typing import Any, AnyStr, Generic, NamedTuple, overload
+from typing_extensions import TypeAlias
+
+__all__ = [
+ "GEN_DELIMS",
+ "RESERVED",
+ "SUB_DELIMS",
+ "UNRESERVED",
+ "isabspath",
+ "isabsuri",
+ "isnetpath",
+ "isrelpath",
+ "issamedoc",
+ "isuri",
+ "uricompose",
+ "uridecode",
+ "uridefrag",
+ "uriencode",
+ "urijoin",
+ "urisplit",
+ "uriunsplit",
+]
+__version__: str
+
+GEN_DELIMS: str
+SUB_DELIMS: str
+RESERVED: str
+UNRESERVED: str
+
+@overload
+def uriencode(
+ uristring: str,
+ safe: str | bytes = ...,
+ encoding: str = ...,
+ errors: str = ...,
+) -> bytes: ...
+@overload
+def uriencode(
+ uristring: bytes,
+ safe: str | bytes = ...,
+ encoding: str | None = ...,
+ errors: str = ...,
+) -> bytes: ...
+@overload
+def uridecode(
+ uristring: str | bytes,
+ encoding: str = ...,
+ errors: str = ...,
+) -> str: ...
+@overload
+def uridecode(
+ uristring: str | bytes,
+ encoding: None,
+ errors: str = ...,
+) -> bytes: ...
+
+class DefragResult(NamedTuple, Generic[AnyStr]):
+ uri: AnyStr
+ fragment: AnyStr | None
+ def geturi(self) -> AnyStr: ...
+ @overload
+ def getfragment(
+ self,
+ default: str | None = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> str | None: ...
+ @overload
+ def getfragment(
+ self,
+ default: bytes | None = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> bytes | None: ...
+
+class SplitResult(NamedTuple, Generic[AnyStr]):
+ scheme: AnyStr | None
+ authority: AnyStr | None
+ path: AnyStr
+ query: AnyStr | None
+ fragment: AnyStr | None
+ @property
+ def userinfo(self) -> AnyStr | None: ...
+ @property
+ def host(self) -> AnyStr | None: ...
+ @property
+ def port(self) -> AnyStr | None: ...
+ def geturi(self) -> AnyStr: ...
+ def getscheme(self, default: str | None = ...) -> str | None: ...
+ @overload
+ def getauthority(
+ self,
+ default: tuple[Any, Any, Any] | None = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> tuple[
+ str | None,
+ str | ipaddress.IPv4Address | ipaddress.IPv6Address | None,
+ int | None,
+ ]: ...
+ @overload
+ def getauthority(
+ self,
+ default: tuple[Any, Any, Any] | None = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> tuple[
+ bytes | None,
+ str | ipaddress.IPv4Address | ipaddress.IPv6Address | None,
+ int | None,
+ ]: ...
+ @overload
+ def getuserinfo(
+ self,
+ default: str | None = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> str | None: ...
+ @overload
+ def getuserinfo(
+ self,
+ default: bytes | None = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> bytes | None: ...
+ def gethost(
+ self,
+ default: str | ipaddress.IPv4Address | ipaddress.IPv6Address | None =
...,
+ errors: str = ...,
+ ) -> str | ipaddress.IPv4Address | ipaddress.IPv6Address | None: ...
+ def getport(self, default: int | None = ...) -> int | None: ...
+ @overload
+ def getpath(self, encoding: str = ..., errors: str = ...) -> str: ...
+ @overload
+ def getpath(self, encoding: None, errors: str = ...) -> bytes: ...
+ @overload
+ def getquery(
+ self,
+ default: str | None = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> str | None: ...
+ @overload
+ def getquery(
+ self,
+ default: bytes | None = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> bytes | None: ...
+ @overload
+ def getquerydict(
+ self,
+ sep: str | bytes = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> dict[str, list[str | None]]: ...
+ @overload
+ def getquerydict(
+ self,
+ sep: str | bytes = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> dict[bytes, list[bytes | None]]: ...
+ @overload
+ def getquerylist(
+ self,
+ sep: str | bytes = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> list[tuple[str, str | None]]: ...
+ @overload
+ def getquerylist(
+ self,
+ sep: str | bytes = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> list[tuple[bytes, bytes | None]]: ...
+ @overload
+ def getfragment(
+ self,
+ default: str | None = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ) -> str | None: ...
+ @overload
+ def getfragment(
+ self,
+ default: bytes | None = ...,
+ *,
+ encoding: None,
+ errors: str = ...,
+ ) -> bytes | None: ...
+ def isuri(self) -> bool: ...
+ def isabsuri(self) -> bool: ...
+ def isnetpath(self) -> bool: ...
+ def isabspath(self) -> bool: ...
+ def isrelpath(self) -> bool: ...
+ def issamedoc(self) -> bool: ...
+ def transform(self, ref: AnyStr, strict: bool = ...) ->
SplitResult[AnyStr]: ...
+
+def uridefrag(uristring: AnyStr) -> DefragResult[AnyStr]: ...
+def urisplit(uristring: AnyStr) -> SplitResult[AnyStr]: ...
+def uriunsplit(parts: Iterable[AnyStr | None]) -> AnyStr: ...
+@overload
+def urijoin(base: str, ref: str | bytes, strict: bool = ...) -> str: ...
+@overload
+def urijoin(base: bytes, ref: str, strict: bool = ...) -> str: ...
+@overload
+def urijoin(base: bytes, ref: bytes, strict: bool = ...) -> bytes: ...
+def isuri(uristring: str | bytes) -> bool: ...
+def isabsuri(uristring: str | bytes) -> bool: ...
+def isnetpath(uristring: str | bytes) -> bool: ...
+def isabspath(uristring: str | bytes) -> bool: ...
+def isrelpath(uristring: str | bytes) -> bool: ...
+def issamedoc(uristring: str | bytes) -> bool: ...
+
+_QueryType: TypeAlias = (
+ str | bytes | Mapping[str | bytes, object] | Iterable[tuple[str | bytes,
object]]
+)
+
+def uricompose(
+ scheme: str | bytes | None = ...,
+ authority: str
+ | bytes
+ | Sequence[str | bytes | ipaddress.IPv4Address | ipaddress.IPv6Address |
int | None]
+ | None = ...,
+ path: str | bytes = ...,
+ query: _QueryType | None = ...,
+ fragment: str | bytes | None = ...,
+ userinfo: str | bytes | None = ...,
+ host: str | bytes | ipaddress.IPv4Address | ipaddress.IPv6Address | None =
...,
+ port: int | str | bytes | None = ...,
+ querysep: str = ...,
+ encoding: str = ...,
+) -> str: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/src/uritools.egg-info/PKG-INFO
new/uritools-6.1.1/src/uritools.egg-info/PKG-INFO
--- old/uritools-6.0.2/src/uritools.egg-info/PKG-INFO 2026-04-22
22:16:59.000000000 +0200
+++ new/uritools-6.1.1/src/uritools.egg-info/PKG-INFO 2026-05-18
20:22:32.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: uritools
-Version: 6.0.2
+Version: 6.1.1
Summary: URI parsing, classification and composition
Author-email: Thomas Kemmer <[email protected]>
Maintainer-email: Thomas Kemmer <[email protected]>
@@ -46,19 +46,10 @@
:target: https://codecov.io/gh/tkem/uritools
:alt: Test coverage
-.. image:: https://img.shields.io/librariesio/sourcerank/pypi/uritools
- :target: https://libraries.io/pypi/uritools
- :alt: Libraries.io SourceRank
-
.. image:: https://img.shields.io/github/license/tkem/uritools
:target: https://raw.github.com/tkem/uritools/master/LICENSE
:alt: License
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/psf/black
- :alt: Code style: black
-
-
This module provides RFC 3986 compliant functions for parsing,
classifying and composing URIs and URI references, largely replacing
the Python Standard Library's ``urllib.parse`` module.
@@ -86,10 +77,9 @@
>>> urijoin(uriunsplit(parts), '/right/here?name=swallow#beak')
'foo://example.com:8042/right/here?name=swallow#beak'
-For various reasons, ``urllib.parse`` and its Python 2 predecessor
-``urlparse`` are not compliant with current Internet standards. As
-stated in `Lib/urllib/parse.py
-<https://github.com/python/cpython/blob/main/Lib/urllib/parse.py#L22>`_:
+For various reasons, ``urllib.parse`` is not compliant with current
+Internet standards. As stated in `Lib/urllib/parse.py
+<https://github.com/python/cpython/blob/3.14/Lib/urllib/parse.py#L22>`_:
RFC 3986 is considered the current standard and any future changes
to urlparse module should conform with it. The urlparse module is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/src/uritools.egg-info/SOURCES.txt
new/uritools-6.1.1/src/uritools.egg-info/SOURCES.txt
--- old/uritools-6.0.2/src/uritools.egg-info/SOURCES.txt 2026-04-22
22:16:59.000000000 +0200
+++ new/uritools-6.1.1/src/uritools.egg-info/SOURCES.txt 2026-05-18
20:22:32.000000000 +0200
@@ -1,13 +1,24 @@
+.gitignore
+.readthedocs.yaml
CHANGELOG.rst
LICENSE
-MANIFEST.in
README.rst
pyproject.toml
tox.ini
-docs/.gitignore
+.github/FUNDING.yml
+.github/SECURITY.md
+.github/copilot-instructions.md
+.github/copilot-review.md
+.github/dependabot.yml
+.github/ISSUE_TEMPLATE/bug_report.md
+.github/ISSUE_TEMPLATE/config.yml
+.github/ISSUE_TEMPLATE/feature_request.md
+.github/workflows/ci.yml
docs/conf.py
docs/index.rst
src/uritools/__init__.py
+src/uritools/__init__.pyi
+src/uritools/py.typed
src/uritools.egg-info/PKG-INFO
src/uritools.egg-info/SOURCES.txt
src/uritools.egg-info/dependency_links.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/tests/test_compose.py
new/uritools-6.1.1/tests/test_compose.py
--- old/uritools-6.0.2/tests/test_compose.py 2026-04-22 22:09:19.000000000
+0200
+++ new/uritools-6.1.1/tests/test_compose.py 2026-05-09 21:51:08.000000000
+0200
@@ -68,7 +68,7 @@
# invalid authority type
for authority in (True, 42, 3.14, ipaddress.IPv6Address("::1")):
with self.assertRaises(TypeError, msg="authority=%r" % authority):
- uricompose(authority=authority)
+ uricompose(authority=authority) # type: ignore
def test_authority_kwargs(self):
from ipaddress import IPv4Address, IPv6Address
@@ -116,9 +116,9 @@
# invalid host type
for host in (True, 42, 3.14, ipaddress.IPv6Network("2001:db00::0/24")):
with self.assertRaises(TypeError, msg="host=%r" % host):
- uricompose(authority=[None, host, None])
+ uricompose(authority=[None, host, None]) # type: ignore
with self.assertRaises(TypeError, msg="host=%r" % host):
- uricompose(host=host)
+ uricompose(host=host) # type: ignore
# invalid host ip-literal
for host in ("[foo]", "[v1.x]"):
with self.assertRaises(ValueError, msg="host=%r" % host):
@@ -128,9 +128,9 @@
# invalid port value
for port in (-1, "foo", 3.14):
with self.assertRaises(ValueError, msg="port=%r" % port):
- uricompose(authority=[None, "", port])
+ uricompose(authority=[None, "", port]) # type: ignore
with self.assertRaises(ValueError, msg="port=%r" % port):
- uricompose(port=port)
+ uricompose(port=port) # type: ignore
def test_authority_override(self):
cases = [
@@ -225,7 +225,7 @@
# invalid query type
for query in (0, [1]):
with self.assertRaises(TypeError, msg="query=%r" % query):
- uricompose(query=query)
+ uricompose(query=query) # type: ignore
def test_query_sep(self):
cases = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/tests/test_defrag.py
new/uritools-6.1.1/tests/test_defrag.py
--- old/uritools-6.0.2/tests/test_defrag.py 2021-10-11 20:18:34.000000000
+0200
+++ new/uritools-6.1.1/tests/test_defrag.py 2026-05-09 21:51:08.000000000
+0200
@@ -41,3 +41,15 @@
self.assertEqual(uridefrag(b"#foo").getfragment(), "foo")
self.assertEqual(uridefrag("#foo%20bar").getfragment(), "foo bar")
self.assertEqual(uridefrag(b"#foo%20bar").getfragment(), "foo bar")
+
+ def test_getfragment_encoding_none(self):
+ self.assertEqual(uridefrag("").getfragment(encoding=None), None)
+ self.assertEqual(uridefrag(b"").getfragment(encoding=None), None)
+ self.assertEqual(uridefrag("#").getfragment(encoding=None), b"")
+ self.assertEqual(uridefrag(b"#").getfragment(encoding=None), b"")
+ self.assertEqual(uridefrag("#foo").getfragment(encoding=None), b"foo")
+ self.assertEqual(uridefrag(b"#foo").getfragment(encoding=None), b"foo")
+ self.assertEqual(uridefrag("#foo%20bar").getfragment(encoding=None),
b"foo bar")
+ self.assertEqual(
+ uridefrag(b"#foo%20bar").getfragment(encoding=None), b"foo bar"
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/tests/test_encoding.py
new/uritools-6.1.1/tests/test_encoding.py
--- old/uritools-6.0.2/tests/test_encoding.py 2026-04-22 22:09:19.000000000
+0200
+++ new/uritools-6.1.1/tests/test_encoding.py 2026-05-18 20:08:33.000000000
+0200
@@ -8,9 +8,7 @@
self.assertEqual(uriencode(decoded, safe, encoding), encoded)
self.assertEqual(uridecode(encoded, encoding), decoded)
# swap bytes/string types
- self.assertEqual(
- uriencode(decoded.encode(encoding), safe, encoding), encoded
- ) # noqa
+ self.assertEqual(uriencode(decoded.encode(encoding), safe, encoding),
encoded) # noqa
self.assertEqual(uridecode(encoded.decode("ascii"), encoding), decoded)
def test_encoding(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/tests/test_split.py
new/uritools-6.1.1/tests/test_split.py
--- old/uritools-6.0.2/tests/test_split.py 2026-04-22 22:09:19.000000000
+0200
+++ new/uritools-6.1.1/tests/test_split.py 2026-05-09 21:51:08.000000000
+0200
@@ -1,3 +1,4 @@
+import ipaddress
import unittest
from uritools import urisplit
@@ -288,10 +289,10 @@
urisplit(uri).getauthority()
with self.assertRaises(ValueError, msg="%r" % uri):
urisplit(uri.encode()).getauthority()
- with self.assertRaises(TypeError, msg="%r" % uri):
- urisplit("").getauthority(42)
- with self.assertRaises(ValueError, msg="%r" % uri):
- urisplit("").getauthority(("userinfo", "test.python.org"))
+ with self.assertRaises(TypeError):
+ urisplit("").getauthority(42) # type: ignore
+ with self.assertRaises(ValueError):
+ urisplit("").getauthority(("userinfo", "test.python.org")) #
type: ignore
def test_gethost(self):
from ipaddress import IPv4Address, IPv6Address
@@ -480,7 +481,9 @@
]
for uri, host, port in cases:
for parts in (urisplit(uri), urisplit(uri.encode("ascii"))):
- self.assertEqual(host, parts.gethost().exploded)
+ result = parts.gethost()
+ assert isinstance(result, ipaddress.IPv6Address)
+ self.assertEqual(host, result.exploded)
self.assertEqual(port, parts.getport())
def test_ipv4_mapped_literal(self):
@@ -538,10 +541,14 @@
]
for uri, hosts, port in cases:
parts = urisplit(uri)
- self.assertIn(parts.gethost().exploded, hosts)
+ result = parts.gethost()
+ assert isinstance(result, ipaddress.IPv6Address)
+ self.assertIn(result.exploded, hosts)
self.assertEqual(parts.getport(), port)
parts = urisplit(uri.encode("ascii"))
- self.assertIn(parts.gethost().exploded, hosts)
+ result = parts.gethost()
+ assert isinstance(result, ipaddress.IPv6Address)
+ self.assertIn(result.exploded, hosts)
self.assertEqual(parts.getport(), port)
def test_invalid_ip_literal(self):
@@ -558,3 +565,38 @@
urisplit(uri).gethost()
with self.assertRaises(ValueError, msg="%r" % uri.encode("ascii")):
urisplit(uri.encode("ascii")).gethost()
+
+ def test_encoding_none(self):
+ uri = "foo://[email protected]:8042/over/there?name=ferret#nose"
+ for ref in [uri, uri.encode("ascii")]:
+ parts = urisplit(ref)
+ self.assertEqual(
+ parts.getauthority(encoding=None),
+ (b"user", "example.com", 8042),
+ )
+ self.assertEqual(parts.getuserinfo(encoding=None), b"user")
+ self.assertEqual(parts.getpath(encoding=None), b"/over/there")
+ self.assertEqual(parts.getquery(encoding=None), b"name=ferret")
+ self.assertEqual(parts.getfragment(encoding=None), b"nose")
+ self.assertEqual(parts.getquerylist(encoding=None), [(b"name",
b"ferret")])
+ self.assertEqual(
+ dict(parts.getquerydict(encoding=None)), {b"name": [b"ferret"]}
+ )
+
+ def test_encoding_none_percent(self):
+ uri = "foo:///foo%20bar?name=foo%20bar#foo%20bar"
+ for ref in [uri, uri.encode("ascii")]:
+ parts = urisplit(ref)
+ self.assertEqual(parts.getpath(encoding=None), b"/foo bar")
+ self.assertEqual(parts.getquery(encoding=None), b"name=foo bar")
+ self.assertEqual(parts.getfragment(encoding=None), b"foo bar")
+ self.assertEqual(parts.getquerylist(encoding=None), [(b"name",
b"foo bar")])
+
+ def test_encoding_none_defaults(self):
+ parts = urisplit("")
+ self.assertEqual(parts.getuserinfo(encoding=None), None)
+ self.assertEqual(parts.getquery(encoding=None), None)
+ self.assertEqual(parts.getfragment(encoding=None), None)
+ self.assertEqual(parts.getpath(encoding=None), b"")
+ self.assertEqual(parts.getquerylist(encoding=None), [])
+ self.assertEqual(dict(parts.getquerydict(encoding=None)), {})
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/uritools-6.0.2/tox.ini new/uritools-6.1.1/tox.ini
--- old/uritools-6.0.2/tox.ini 2025-12-21 19:41:35.000000000 +0100
+++ new/uritools-6.1.1/tox.ini 2026-05-18 20:08:33.000000000 +0200
@@ -1,5 +1,5 @@
[tox]
-envlist = check-manifest,docs,doctest,flake8,py
+envlist = py,docs,doctest,ruff,ruff-format,pyright
[testenv]
deps =
@@ -8,13 +8,6 @@
commands =
py.test --basetemp={envtmpdir} --cov=uritools --cov-report term-missing
{posargs}
-[testenv:check-manifest]
-deps =
- check-manifest
-commands =
- check-manifest
-skip_install = true
-
[testenv:docs]
deps =
sphinx
@@ -27,13 +20,23 @@
commands =
sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs
{envtmpdir}/doctest
-[testenv:flake8]
+[testenv:ruff]
+deps =
+ ruff
+commands =
+ ruff check {posargs}
+skip_install = true
+
+[testenv:ruff-format]
+deps =
+ ruff
+commands =
+ ruff format --diff -q {posargs}
+skip_install = true
+
+[testenv:pyright]
deps =
- flake8
- flake8-black
- flake8-bugbear
- flake8-import-order
- flake8-pyproject
+ pyright
commands =
- flake8
+ pyright {posargs}
skip_install = true