url-normalize 2.2.0__tar.gz → 2.2.1__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.

Potentially problematic release.


This version of url-normalize might be problematic. Click here for more details.

Files changed (52) hide show
  1. {url_normalize-2.2.0 → url_normalize-2.2.1}/PKG-INFO +56 -31
  2. {url_normalize-2.2.0 → url_normalize-2.2.1}/README.md +56 -29
  3. {url_normalize-2.2.0 → url_normalize-2.2.1}/pyproject.toml +3 -14
  4. url_normalize-2.2.1/tests/test_deconstruct_url.py +40 -0
  5. url_normalize-2.2.1/tests/test_normalize_fragment.py +23 -0
  6. url_normalize-2.2.1/tests/test_normalize_host.py +32 -0
  7. url_normalize-2.2.1/tests/test_normalize_path.py +44 -0
  8. url_normalize-2.2.1/tests/test_normalize_port.py +26 -0
  9. url_normalize-2.2.1/tests/test_normalize_scheme.py +18 -0
  10. url_normalize-2.2.1/tests/test_normalize_userinfo.py +21 -0
  11. url_normalize-2.2.1/tests/test_provide_url_scheme.py +32 -0
  12. url_normalize-2.2.1/tests/test_reconstruct_url.py +42 -0
  13. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/__init__.py +1 -1
  14. url_normalize-2.2.1/url_normalize/py.typed +0 -0
  15. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/PKG-INFO +56 -31
  16. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/SOURCES.txt +1 -0
  17. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/requires.txt +0 -2
  18. url_normalize-2.2.0/tests/test_deconstruct_url.py +0 -35
  19. url_normalize-2.2.0/tests/test_normalize_fragment.py +0 -21
  20. url_normalize-2.2.0/tests/test_normalize_host.py +0 -29
  21. url_normalize-2.2.0/tests/test_normalize_path.py +0 -42
  22. url_normalize-2.2.0/tests/test_normalize_port.py +0 -13
  23. url_normalize-2.2.0/tests/test_normalize_scheme.py +0 -13
  24. url_normalize-2.2.0/tests/test_normalize_userinfo.py +0 -19
  25. url_normalize-2.2.0/tests/test_provide_url_scheme.py +0 -30
  26. url_normalize-2.2.0/tests/test_reconstruct_url.py +0 -41
  27. {url_normalize-2.2.0 → url_normalize-2.2.1}/LICENSE +0 -0
  28. {url_normalize-2.2.0 → url_normalize-2.2.1}/setup.cfg +0 -0
  29. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_cli.py +0 -0
  30. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_generic_url_cleanup.py +0 -0
  31. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_normalize_query.py +0 -0
  32. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_normalize_query_filters.py +0 -0
  33. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_provide_url_domain.py +0 -0
  34. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_tools.py +0 -0
  35. {url_normalize-2.2.0 → url_normalize-2.2.1}/tests/test_url_normalize.py +0 -0
  36. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/cli.py +0 -0
  37. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/generic_url_cleanup.py +0 -0
  38. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_fragment.py +0 -0
  39. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_host.py +0 -0
  40. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_path.py +0 -0
  41. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_port.py +0 -0
  42. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_query.py +0 -0
  43. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_scheme.py +0 -0
  44. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/normalize_userinfo.py +0 -0
  45. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/param_allowlist.py +0 -0
  46. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/provide_url_domain.py +0 -0
  47. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/provide_url_scheme.py +0 -0
  48. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/tools.py +0 -0
  49. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize/url_normalize.py +0 -0
  50. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/dependency_links.txt +0 -0
  51. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/entry_points.txt +0 -0
  52. {url_normalize-2.2.0 → url_normalize-2.2.1}/url_normalize.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: url-normalize
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: URL normalization for Python
5
5
  Author-email: Nikolay Panov <github@npanov.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/niksite/url-normalize
8
8
  Project-URL: Repository, https://github.com/niksite/url-normalize
9
9
  Project-URL: Issues, https://github.com/niksite/url-normalize/issues
10
10
  Project-URL: Changelog, https://github.com/niksite/url-normalize/blob/master/CHANGELOG.md
11
- Keywords: url,normalization,normalize
11
+ Keywords: url,normalization,normalize,normalizer
12
12
  Requires-Python: >=3.8
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
@@ -17,48 +17,67 @@ Provides-Extra: dev
17
17
  Requires-Dist: mypy; extra == "dev"
18
18
  Requires-Dist: pre-commit; extra == "dev"
19
19
  Requires-Dist: pytest-cov; extra == "dev"
20
- Requires-Dist: pytest-ruff; extra == "dev"
21
20
  Requires-Dist: pytest-socket; extra == "dev"
22
21
  Requires-Dist: pytest; extra == "dev"
23
22
  Requires-Dist: ruff; extra == "dev"
24
- Requires-Dist: tox; extra == "dev"
25
23
  Dynamic: license-file
26
24
 
27
25
  # url-normalize
28
26
 
29
- [![CI](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
30
- [![PyPI](https://github.com/niksite/url-normalize/actions/workflows/publish.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/publish.yml)
27
+ [![tests](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
28
+ [![Coveralls](https://img.shields.io/coveralls/github/niksite/url-normalize/master.svg)](https://coveralls.io/r/niksite/url-normalize)
29
+ [![PyPI](https://img.shields.io/pypi/v/url-normalize.svg)](https://pypi.org/project/url-normalize/)
31
30
 
32
- URI Normalization function:
31
+ A Python library for standardizing and normalizing URLs with support for internationalized domain names (IDN).
33
32
 
34
- * Take care of IDN domains.
35
- * Always provide the URI scheme in lowercase characters.
36
- * Always provide the host, if any, in lowercase characters.
37
- * Only perform percent-encoding where it is essential.
38
- * Always use uppercase A-through-F characters when percent-encoding.
39
- * Prevent dot-segments appearing in non-relative URI paths.
40
- * For schemes that define a default authority, use an empty authority if the
33
+ ## Table of Contents
34
+
35
+ - [Introduction](#introduction)
36
+ - [Features](#features)
37
+ - [Installation](#installation)
38
+ - [Usage](#usage)
39
+ - [Python API](#python-api)
40
+ - [Command Line](#command-line-usage)
41
+ - [Documentation](#documentation)
42
+ - [Contributing](#contributing)
43
+ - [License](#license)
44
+
45
+ ## Introduction
46
+
47
+ url-normalize provides a robust URI normalization function that:
48
+
49
+ - Takes care of IDN domains.
50
+ - Always provides the URI scheme in lowercase characters.
51
+ - Always provides the host, if any, in lowercase characters.
52
+ - Only performs percent-encoding where it is essential.
53
+ - Always uses uppercase A-through-F characters when percent-encoding.
54
+ - Prevents dot-segments appearing in non-relative URI paths.
55
+ - For schemes that define a default authority, uses an empty authority if the
41
56
  default is desired.
42
- * For schemes that define an empty path to be equivalent to a path of "/",
43
- use "/".
44
- * For schemes that define a port, use an empty port if the default is desired
45
- * All portions of the URI must be utf-8 encoded NFC from Unicode strings
57
+ - For schemes that define an empty path to be equivalent to a path of "/",
58
+ uses "/".
59
+ - For schemes that define a port, uses an empty port if the default is desired
60
+ - Ensures all portions of the URI are utf-8 encoded NFC from Unicode strings
46
61
 
47
- Inspired by Sam Ruby's [urlnorm.py](<http://intertwingly.net/blog/2004/08/04/Urlnorm>)
62
+ Inspired by Sam Ruby's [urlnorm.py](http://intertwingly.net/blog/2004/08/04/Urlnorm)
48
63
 
49
64
  ## Features
50
65
 
51
- * IDN (Internationalized Domain Name) support
52
- * Configurable default scheme (https by default)
53
- * Configurable default domain for absolute paths
54
- * Query parameter filtering with allowlists
55
- * Support for various URL formats including:
56
- * Empty string URLs
57
- * Double slash URLs (//domain.tld)
58
- * Shebang (#!) URLs
59
- * Cross-version Python compatibility (3.8+)
60
- * 100% test coverage
61
- * Modern type hints and string handling
66
+ - **IDN Support**: Full internationalized domain name handling
67
+ - **Configurable Defaults**:
68
+ - Customizable default scheme (https by default)
69
+ - Configurable default domain for absolute paths
70
+ - **Query Parameter Control**:
71
+ - Parameter filtering with allowlists
72
+ - Support for domain-specific parameter rules
73
+ - **Versatile URL Handling**:
74
+ - Empty string URLs
75
+ - Double slash URLs (//domain.tld)
76
+ - Shebang (#!) URLs
77
+ - **Developer Friendly**:
78
+ - Cross-version Python compatibility (3.8+)
79
+ - 100% test coverage
80
+ - Modern type hints and string handling
62
81
 
63
82
  ## Installation
64
83
 
@@ -68,6 +87,8 @@ pip install url-normalize
68
87
 
69
88
  ## Usage
70
89
 
90
+ ### Python API
91
+
71
92
  ```python
72
93
  from url_normalize import url_normalize
73
94
 
@@ -108,7 +129,7 @@ print(url_normalize("/images/logo.png", default_scheme="http", default_domain="e
108
129
  # Output: http://example.com/images/logo.png
109
130
  ```
110
131
 
111
- ### Command-line usage
132
+ ### Command-line Usage
112
133
 
113
134
  You can also use `url-normalize` from the command line:
114
135
 
@@ -145,6 +166,10 @@ $ uvx url-normalize www.foo.com:80/foo
145
166
 
146
167
  For a complete history of changes, see [CHANGELOG.md](CHANGELOG.md).
147
168
 
169
+ ## Contributing
170
+
171
+ Contributions are welcome! Please feel free to submit a Pull Request.
172
+
148
173
  ## License
149
174
 
150
175
  MIT License
@@ -1,38 +1,59 @@
1
1
  # url-normalize
2
2
 
3
- [![CI](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
4
- [![PyPI](https://github.com/niksite/url-normalize/actions/workflows/publish.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/publish.yml)
5
-
6
- URI Normalization function:
7
-
8
- * Take care of IDN domains.
9
- * Always provide the URI scheme in lowercase characters.
10
- * Always provide the host, if any, in lowercase characters.
11
- * Only perform percent-encoding where it is essential.
12
- * Always use uppercase A-through-F characters when percent-encoding.
13
- * Prevent dot-segments appearing in non-relative URI paths.
14
- * For schemes that define a default authority, use an empty authority if the
3
+ [![tests](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
4
+ [![Coveralls](https://img.shields.io/coveralls/github/niksite/url-normalize/master.svg)](https://coveralls.io/r/niksite/url-normalize)
5
+ [![PyPI](https://img.shields.io/pypi/v/url-normalize.svg)](https://pypi.org/project/url-normalize/)
6
+
7
+ A Python library for standardizing and normalizing URLs with support for internationalized domain names (IDN).
8
+
9
+ ## Table of Contents
10
+
11
+ - [Introduction](#introduction)
12
+ - [Features](#features)
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Python API](#python-api)
16
+ - [Command Line](#command-line-usage)
17
+ - [Documentation](#documentation)
18
+ - [Contributing](#contributing)
19
+ - [License](#license)
20
+
21
+ ## Introduction
22
+
23
+ url-normalize provides a robust URI normalization function that:
24
+
25
+ - Takes care of IDN domains.
26
+ - Always provides the URI scheme in lowercase characters.
27
+ - Always provides the host, if any, in lowercase characters.
28
+ - Only performs percent-encoding where it is essential.
29
+ - Always uses uppercase A-through-F characters when percent-encoding.
30
+ - Prevents dot-segments appearing in non-relative URI paths.
31
+ - For schemes that define a default authority, uses an empty authority if the
15
32
  default is desired.
16
- * For schemes that define an empty path to be equivalent to a path of "/",
17
- use "/".
18
- * For schemes that define a port, use an empty port if the default is desired
19
- * All portions of the URI must be utf-8 encoded NFC from Unicode strings
33
+ - For schemes that define an empty path to be equivalent to a path of "/",
34
+ uses "/".
35
+ - For schemes that define a port, uses an empty port if the default is desired
36
+ - Ensures all portions of the URI are utf-8 encoded NFC from Unicode strings
20
37
 
21
- Inspired by Sam Ruby's [urlnorm.py](<http://intertwingly.net/blog/2004/08/04/Urlnorm>)
38
+ Inspired by Sam Ruby's [urlnorm.py](http://intertwingly.net/blog/2004/08/04/Urlnorm)
22
39
 
23
40
  ## Features
24
41
 
25
- * IDN (Internationalized Domain Name) support
26
- * Configurable default scheme (https by default)
27
- * Configurable default domain for absolute paths
28
- * Query parameter filtering with allowlists
29
- * Support for various URL formats including:
30
- * Empty string URLs
31
- * Double slash URLs (//domain.tld)
32
- * Shebang (#!) URLs
33
- * Cross-version Python compatibility (3.8+)
34
- * 100% test coverage
35
- * Modern type hints and string handling
42
+ - **IDN Support**: Full internationalized domain name handling
43
+ - **Configurable Defaults**:
44
+ - Customizable default scheme (https by default)
45
+ - Configurable default domain for absolute paths
46
+ - **Query Parameter Control**:
47
+ - Parameter filtering with allowlists
48
+ - Support for domain-specific parameter rules
49
+ - **Versatile URL Handling**:
50
+ - Empty string URLs
51
+ - Double slash URLs (//domain.tld)
52
+ - Shebang (#!) URLs
53
+ - **Developer Friendly**:
54
+ - Cross-version Python compatibility (3.8+)
55
+ - 100% test coverage
56
+ - Modern type hints and string handling
36
57
 
37
58
  ## Installation
38
59
 
@@ -42,6 +63,8 @@ pip install url-normalize
42
63
 
43
64
  ## Usage
44
65
 
66
+ ### Python API
67
+
45
68
  ```python
46
69
  from url_normalize import url_normalize
47
70
 
@@ -82,7 +105,7 @@ print(url_normalize("/images/logo.png", default_scheme="http", default_domain="e
82
105
  # Output: http://example.com/images/logo.png
83
106
  ```
84
107
 
85
- ### Command-line usage
108
+ ### Command-line Usage
86
109
 
87
110
  You can also use `url-normalize` from the command line:
88
111
 
@@ -119,6 +142,10 @@ $ uvx url-normalize www.foo.com:80/foo
119
142
 
120
143
  For a complete history of changes, see [CHANGELOG.md](CHANGELOG.md).
121
144
 
145
+ ## Contributing
146
+
147
+ Contributions are welcome! Please feel free to submit a Pull Request.
148
+
122
149
  ## License
123
150
 
124
151
  MIT License
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "url-normalize"
3
- version = "2.2.0"
3
+ version = "2.2.1"
4
4
  description = "URL normalization for Python"
5
5
  authors = [{ name = "Nikolay Panov", email = "github@npanov.com" }]
6
6
  license = { text = "MIT" }
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.8"
9
- keywords = ["url", "normalization", "normalize"]
9
+ keywords = ["url", "normalization", "normalize", "normalizer"]
10
10
  dependencies = ["idna>=3.3"]
11
11
 
12
12
  [project.urls]
@@ -19,16 +19,7 @@ Changelog = "https://github.com/niksite/url-normalize/blob/master/CHANGELOG.md"
19
19
  url-normalize = "url_normalize.cli:main"
20
20
 
21
21
  [project.optional-dependencies]
22
- dev = [
23
- "mypy",
24
- "pre-commit",
25
- "pytest-cov",
26
- "pytest-ruff",
27
- "pytest-socket",
28
- "pytest",
29
- "ruff",
30
- "tox",
31
- ]
22
+ dev = ["mypy", "pre-commit", "pytest-cov", "pytest-socket", "pytest", "ruff"]
32
23
 
33
24
  [tool.ruff]
34
25
  target-version = "py38"
@@ -71,11 +62,9 @@ build-backend = "setuptools.build_meta"
71
62
 
72
63
  [tool.pytest.ini_options]
73
64
  addopts = [
74
- "--cov-fail-under=100",
75
65
  "--cov-report=term-missing:skip-covered",
76
66
  "--cov=url_normalize",
77
67
  "--disable-socket",
78
- "--ruff",
79
68
  "-v",
80
69
  ]
81
70
  python_files = ["tests.py", "test_*.py", "*_tests.py"]
@@ -0,0 +1,40 @@
1
+ """Deconstruct url tests."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.tools import URL, deconstruct_url
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("url", "expected"),
10
+ [
11
+ (
12
+ "http://site.com",
13
+ URL(
14
+ fragment="",
15
+ host="site.com",
16
+ path="",
17
+ port="",
18
+ query="",
19
+ scheme="http",
20
+ userinfo="",
21
+ ),
22
+ ),
23
+ (
24
+ "http://user@www.example.com:8080/path/index.html?param=val#fragment",
25
+ URL(
26
+ fragment="fragment",
27
+ host="www.example.com",
28
+ path="/path/index.html",
29
+ port="8080",
30
+ query="param=val",
31
+ scheme="http",
32
+ userinfo="user@",
33
+ ),
34
+ ),
35
+ ],
36
+ )
37
+ def test_deconstruct_url_result_is_expected(url: str, expected: URL) -> None:
38
+ """Assert we got expected results from the deconstruct_url function."""
39
+ result = deconstruct_url(url)
40
+ assert result == expected, url
@@ -0,0 +1,23 @@
1
+ """Tests for normalize_fragment function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_fragment
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("fragment", "expected"),
10
+ [
11
+ ("", ""),
12
+ ("fragment", "fragment"),
13
+ ("пример", "%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80"),
14
+ ("!fragment", "%21fragment"),
15
+ ("~fragment", "~fragment"),
16
+ # Issue #36: Equal sign should not be encoded
17
+ ("gid=1234", "gid=1234"),
18
+ ],
19
+ )
20
+ def test_normalize_fragment_result_is_expected(fragment: str, expected: str) -> None:
21
+ """Assert we got expected results from the normalize_fragment function."""
22
+ result = normalize_fragment(fragment)
23
+ assert result == expected, fragment
@@ -0,0 +1,32 @@
1
+ """Tests for normalize_host function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_host
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("host", "expected"),
10
+ [
11
+ # Basic cases
12
+ ("site.com", "site.com"),
13
+ ("SITE.COM", "site.com"),
14
+ ("site.com.", "site.com"),
15
+ # Cyrillic domains
16
+ ("пример.испытание", "xn--e1afmkfd.xn--80akhbyknj4f"),
17
+ # Mixed case with Cyrillic
18
+ ("ExAmPle.РФ", "example.xn--p1ai"),
19
+ # IDNA2008 with UTS46
20
+ ("faß.de", "fass.de"), # Normalize using transitional rules
21
+ # Edge cases
22
+ ("ドメイン.テスト", "xn--eckwd4c7c.xn--zckzah"), # Japanese
23
+ ("domain.café", "domain.xn--caf-dma"), # Latin with diacritic
24
+ # Normalization tests
25
+ ("über.example", "xn--ber-goa.example"), # IDNA 2008 for umlaut
26
+ ("example。com", "example.com"), # Normalize full-width punctuation
27
+ ],
28
+ )
29
+ def test_normalize_host_result_is_expected(host: str, expected: str) -> None:
30
+ """Assert we got expected results from the normalize_host function."""
31
+ result = normalize_host(host)
32
+ assert result == expected, host
@@ -0,0 +1,44 @@
1
+ """Tests for normalize_path function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_path
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("path", "expected"),
10
+ [
11
+ ("..", "/"),
12
+ ("", "/"),
13
+ ("/../foo", "/foo"),
14
+ ("/..foo", "/..foo"),
15
+ ("/./../foo", "/foo"),
16
+ ("/./foo", "/foo"),
17
+ ("/./foo/.", "/foo/"),
18
+ ("/.foo", "/.foo"),
19
+ ("/", "/"),
20
+ ("/foo..", "/foo.."),
21
+ ("/foo.", "/foo."),
22
+ ("/FOO", "/FOO"),
23
+ ("/foo/../bar", "/bar"),
24
+ ("/foo/./bar", "/foo/bar"),
25
+ ("/foo//", "/foo/"),
26
+ ("/foo///bar//", "/foo/bar/"),
27
+ ("/foo/bar/..", "/foo/"),
28
+ ("/foo/bar/../..", "/"),
29
+ ("/foo/bar/../../../../baz", "/baz"),
30
+ ("/foo/bar/../../../baz", "/baz"),
31
+ ("/foo/bar/../../", "/"),
32
+ ("/foo/bar/../../baz", "/baz"),
33
+ ("/foo/bar/../", "/foo/"),
34
+ ("/foo/bar/../baz", "/foo/baz"),
35
+ ("/foo/bar/.", "/foo/bar/"),
36
+ ("/foo/bar/./", "/foo/bar/"),
37
+ # Issue #25: we should preserve ? in the path
38
+ ("/More+Tea+Vicar%3F/discussion", "/More+Tea+Vicar%3F/discussion"),
39
+ ],
40
+ )
41
+ def test_normalize_path_result_is_expected(path: str, expected: str) -> None:
42
+ """Assert we got expected results from the normalize_path function."""
43
+ result = normalize_path(path, "http")
44
+ assert result == expected, path
@@ -0,0 +1,26 @@
1
+ """Tests for normalize_port function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_port
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("port", "expected"),
10
+ [
11
+ ("8080", "8080"), # Non-default port
12
+ ("", ""), # Empty port
13
+ ("80", ""), # Default HTTP port
14
+ ("string", "string"), # Non-numeric port (should pass through)
15
+ # Add more cases as needed, e.g., for HTTPS
16
+ pytest.param("443", "", id="https_default_port"),
17
+ ],
18
+ )
19
+ def test_normalize_port_result_is_expected(port: str, expected: str):
20
+ """Assert we got expected results from the normalize_port function."""
21
+ # Test with 'http' scheme for most cases
22
+ scheme = "https" if port == "443" else "http"
23
+
24
+ result = normalize_port(port, scheme)
25
+
26
+ assert result == expected
@@ -0,0 +1,18 @@
1
+ """Tests for normalize_scheme function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_scheme
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("scheme", "expected"),
10
+ [
11
+ ("http", "http"),
12
+ ("HTTP", "http"),
13
+ ],
14
+ )
15
+ def test_normalize_scheme_result_is_expected(scheme: str, expected: str) -> None:
16
+ """Assert we got expected results from the normalize_scheme function."""
17
+ result = normalize_scheme(scheme)
18
+ assert result == expected, scheme
@@ -0,0 +1,21 @@
1
+ """Tests for normalize_userinfo function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import normalize_userinfo
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("userinfo", "expected"),
10
+ [
11
+ (":@", ""),
12
+ ("", ""),
13
+ ("@", ""),
14
+ ("user:password@", "user:password@"),
15
+ ("user@", "user@"),
16
+ ],
17
+ )
18
+ def test_normalize_userinfo_result_is_expected(userinfo: str, expected: str) -> None:
19
+ """Assert we got expected results from the normalize_userinfo function."""
20
+ result = normalize_userinfo(userinfo)
21
+ assert result == expected, userinfo
@@ -0,0 +1,32 @@
1
+ """Tests for provide_url_scheme function."""
2
+
3
+ import pytest
4
+
5
+ from url_normalize.url_normalize import provide_url_scheme
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ ("url", "expected"),
10
+ [
11
+ ("", ""),
12
+ ("-", "-"),
13
+ ("/file/path", "/file/path"),
14
+ ("//site/path", "https://site/path"),
15
+ ("ftp://site/", "ftp://site/"),
16
+ ("site/page", "https://site/page"),
17
+ ],
18
+ )
19
+ def test_provide_url_scheme_result_is_expected(url: str, expected: str) -> None:
20
+ """Assert we got expected results from the provide_url_scheme function."""
21
+ result = provide_url_scheme(url)
22
+ assert result == expected, url
23
+
24
+
25
+ def test_provide_url_scheme_accept_default_scheme_param() -> None:
26
+ """Assert we could provide default_scheme param other than https."""
27
+ url = "//site/path"
28
+ expected = "http://site/path"
29
+
30
+ actual = provide_url_scheme(url, default_scheme="http")
31
+
32
+ assert actual == expected
@@ -0,0 +1,42 @@
1
+ """Reconstruct url tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from url_normalize.tools import URL, reconstruct_url
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("url_obj", "expected"),
12
+ [
13
+ (
14
+ URL(
15
+ fragment="",
16
+ host="site.com",
17
+ path="",
18
+ port="",
19
+ query="",
20
+ scheme="http",
21
+ userinfo="",
22
+ ),
23
+ "http://site.com",
24
+ ),
25
+ (
26
+ URL(
27
+ fragment="fragment",
28
+ host="www.example.com",
29
+ path="/path/index.html",
30
+ port="8080",
31
+ query="param=val",
32
+ scheme="http",
33
+ userinfo="user@",
34
+ ),
35
+ "http://user@www.example.com:8080/path/index.html?param=val#fragment",
36
+ ),
37
+ ],
38
+ )
39
+ def test_reconstruct_url_result_is_expected(url_obj: URL, expected: str) -> None:
40
+ """Assert we got expected results from the reconstruct_url function."""
41
+ result = reconstruct_url(url_obj)
42
+ assert result == expected, url_obj
@@ -8,6 +8,6 @@ SPDX-License-Identifier: MIT
8
8
  from .url_normalize import url_normalize
9
9
 
10
10
  __license__ = "MIT"
11
- __version__ = "2.2.0"
11
+ __version__ = "2.2.1"
12
12
 
13
13
  __all__ = ["url_normalize"]
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: url-normalize
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: URL normalization for Python
5
5
  Author-email: Nikolay Panov <github@npanov.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/niksite/url-normalize
8
8
  Project-URL: Repository, https://github.com/niksite/url-normalize
9
9
  Project-URL: Issues, https://github.com/niksite/url-normalize/issues
10
10
  Project-URL: Changelog, https://github.com/niksite/url-normalize/blob/master/CHANGELOG.md
11
- Keywords: url,normalization,normalize
11
+ Keywords: url,normalization,normalize,normalizer
12
12
  Requires-Python: >=3.8
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
@@ -17,48 +17,67 @@ Provides-Extra: dev
17
17
  Requires-Dist: mypy; extra == "dev"
18
18
  Requires-Dist: pre-commit; extra == "dev"
19
19
  Requires-Dist: pytest-cov; extra == "dev"
20
- Requires-Dist: pytest-ruff; extra == "dev"
21
20
  Requires-Dist: pytest-socket; extra == "dev"
22
21
  Requires-Dist: pytest; extra == "dev"
23
22
  Requires-Dist: ruff; extra == "dev"
24
- Requires-Dist: tox; extra == "dev"
25
23
  Dynamic: license-file
26
24
 
27
25
  # url-normalize
28
26
 
29
- [![CI](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
30
- [![PyPI](https://github.com/niksite/url-normalize/actions/workflows/publish.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/publish.yml)
27
+ [![tests](https://github.com/niksite/url-normalize/actions/workflows/ci.yml/badge.svg)](https://github.com/niksite/url-normalize/actions/workflows/ci.yml)
28
+ [![Coveralls](https://img.shields.io/coveralls/github/niksite/url-normalize/master.svg)](https://coveralls.io/r/niksite/url-normalize)
29
+ [![PyPI](https://img.shields.io/pypi/v/url-normalize.svg)](https://pypi.org/project/url-normalize/)
31
30
 
32
- URI Normalization function:
31
+ A Python library for standardizing and normalizing URLs with support for internationalized domain names (IDN).
33
32
 
34
- * Take care of IDN domains.
35
- * Always provide the URI scheme in lowercase characters.
36
- * Always provide the host, if any, in lowercase characters.
37
- * Only perform percent-encoding where it is essential.
38
- * Always use uppercase A-through-F characters when percent-encoding.
39
- * Prevent dot-segments appearing in non-relative URI paths.
40
- * For schemes that define a default authority, use an empty authority if the
33
+ ## Table of Contents
34
+
35
+ - [Introduction](#introduction)
36
+ - [Features](#features)
37
+ - [Installation](#installation)
38
+ - [Usage](#usage)
39
+ - [Python API](#python-api)
40
+ - [Command Line](#command-line-usage)
41
+ - [Documentation](#documentation)
42
+ - [Contributing](#contributing)
43
+ - [License](#license)
44
+
45
+ ## Introduction
46
+
47
+ url-normalize provides a robust URI normalization function that:
48
+
49
+ - Takes care of IDN domains.
50
+ - Always provides the URI scheme in lowercase characters.
51
+ - Always provides the host, if any, in lowercase characters.
52
+ - Only performs percent-encoding where it is essential.
53
+ - Always uses uppercase A-through-F characters when percent-encoding.
54
+ - Prevents dot-segments appearing in non-relative URI paths.
55
+ - For schemes that define a default authority, uses an empty authority if the
41
56
  default is desired.
42
- * For schemes that define an empty path to be equivalent to a path of "/",
43
- use "/".
44
- * For schemes that define a port, use an empty port if the default is desired
45
- * All portions of the URI must be utf-8 encoded NFC from Unicode strings
57
+ - For schemes that define an empty path to be equivalent to a path of "/",
58
+ uses "/".
59
+ - For schemes that define a port, uses an empty port if the default is desired
60
+ - Ensures all portions of the URI are utf-8 encoded NFC from Unicode strings
46
61
 
47
- Inspired by Sam Ruby's [urlnorm.py](<http://intertwingly.net/blog/2004/08/04/Urlnorm>)
62
+ Inspired by Sam Ruby's [urlnorm.py](http://intertwingly.net/blog/2004/08/04/Urlnorm)
48
63
 
49
64
  ## Features
50
65
 
51
- * IDN (Internationalized Domain Name) support
52
- * Configurable default scheme (https by default)
53
- * Configurable default domain for absolute paths
54
- * Query parameter filtering with allowlists
55
- * Support for various URL formats including:
56
- * Empty string URLs
57
- * Double slash URLs (//domain.tld)
58
- * Shebang (#!) URLs
59
- * Cross-version Python compatibility (3.8+)
60
- * 100% test coverage
61
- * Modern type hints and string handling
66
+ - **IDN Support**: Full internationalized domain name handling
67
+ - **Configurable Defaults**:
68
+ - Customizable default scheme (https by default)
69
+ - Configurable default domain for absolute paths
70
+ - **Query Parameter Control**:
71
+ - Parameter filtering with allowlists
72
+ - Support for domain-specific parameter rules
73
+ - **Versatile URL Handling**:
74
+ - Empty string URLs
75
+ - Double slash URLs (//domain.tld)
76
+ - Shebang (#!) URLs
77
+ - **Developer Friendly**:
78
+ - Cross-version Python compatibility (3.8+)
79
+ - 100% test coverage
80
+ - Modern type hints and string handling
62
81
 
63
82
  ## Installation
64
83
 
@@ -68,6 +87,8 @@ pip install url-normalize
68
87
 
69
88
  ## Usage
70
89
 
90
+ ### Python API
91
+
71
92
  ```python
72
93
  from url_normalize import url_normalize
73
94
 
@@ -108,7 +129,7 @@ print(url_normalize("/images/logo.png", default_scheme="http", default_domain="e
108
129
  # Output: http://example.com/images/logo.png
109
130
  ```
110
131
 
111
- ### Command-line usage
132
+ ### Command-line Usage
112
133
 
113
134
  You can also use `url-normalize` from the command line:
114
135
 
@@ -145,6 +166,10 @@ $ uvx url-normalize www.foo.com:80/foo
145
166
 
146
167
  For a complete history of changes, see [CHANGELOG.md](CHANGELOG.md).
147
168
 
169
+ ## Contributing
170
+
171
+ Contributions are welcome! Please feel free to submit a Pull Request.
172
+
148
173
  ## License
149
174
 
150
175
  MIT License
@@ -30,6 +30,7 @@ url_normalize/normalize_userinfo.py
30
30
  url_normalize/param_allowlist.py
31
31
  url_normalize/provide_url_domain.py
32
32
  url_normalize/provide_url_scheme.py
33
+ url_normalize/py.typed
33
34
  url_normalize/tools.py
34
35
  url_normalize/url_normalize.py
35
36
  url_normalize.egg-info/PKG-INFO
@@ -4,8 +4,6 @@ idna>=3.3
4
4
  mypy
5
5
  pre-commit
6
6
  pytest-cov
7
- pytest-ruff
8
7
  pytest-socket
9
8
  pytest
10
9
  ruff
11
- tox
@@ -1,35 +0,0 @@
1
- """Deconstruct url tests."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Final
6
-
7
- from url_normalize.tools import URL, deconstruct_url
8
-
9
- EXPECTED_DATA: Final[dict[str, URL]] = {
10
- "http://site.com": URL(
11
- fragment="",
12
- host="site.com",
13
- path="",
14
- port="",
15
- query="",
16
- scheme="http",
17
- userinfo="",
18
- ),
19
- "http://user@www.example.com:8080/path/index.html?param=val#fragment": URL(
20
- fragment="fragment",
21
- host="www.example.com",
22
- path="/path/index.html",
23
- port="8080",
24
- query="param=val",
25
- scheme="http",
26
- userinfo="user@",
27
- ),
28
- }
29
-
30
-
31
- def test_deconstruct_url_result_is_expected() -> None:
32
- """Assert we got expected results from the deconstruct_url function."""
33
- for url, expected in EXPECTED_DATA.items():
34
- result = deconstruct_url(url)
35
- assert result == expected, url
@@ -1,21 +0,0 @@
1
- """Tests for normalize_fragment function."""
2
-
3
- from url_normalize.url_normalize import normalize_fragment
4
-
5
- EXPECTED_DATA = {
6
- "": "",
7
- "fragment": "fragment",
8
- "пример": "%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80",
9
- "!fragment": "%21fragment",
10
- "~fragment": "~fragment",
11
- # Issue #36: Equal sign should not be encoded
12
- "gid=1234": "gid=1234",
13
- }
14
-
15
-
16
- def test_normalize_fragment_result_is_expected():
17
- """Assert we got expected results from the normalize_fragment function."""
18
- for url, expected in EXPECTED_DATA.items():
19
- result = normalize_fragment(url)
20
-
21
- assert result == expected, url
@@ -1,29 +0,0 @@
1
- """Tests for normalize_host function."""
2
-
3
- from url_normalize.url_normalize import normalize_host
4
-
5
- EXPECTED_DATA = {
6
- # Basic cases
7
- "site.com": "site.com",
8
- "SITE.COM": "site.com",
9
- "site.com.": "site.com",
10
- # Cyrillic domains
11
- "пример.испытание": "xn--e1afmkfd.xn--80akhbyknj4f",
12
- # Mixed case with Cyrillic
13
- "ExAmPle.РФ": "example.xn--p1ai",
14
- # IDNA2008 with UTS46
15
- "faß.de": "fass.de", # Normalize using transitional rules
16
- # Edge cases
17
- "ドメイン.テスト": "xn--eckwd4c7c.xn--zckzah", # Japanese
18
- "domain.café": "domain.xn--caf-dma", # Latin with diacritic
19
- # Normalization tests
20
- "über.example": "xn--ber-goa.example", # IDNA 2008 for umlaut
21
- "example。com": "example.com", # Normalize full-width punctuation
22
- }
23
-
24
-
25
- def test_normalize_host_result_is_expected() -> None:
26
- """Assert we got expected results from the normalize_host function."""
27
- for url, expected in EXPECTED_DATA.items():
28
- result = normalize_host(url)
29
- assert result == expected, url
@@ -1,42 +0,0 @@
1
- """Tests for normalize_path function."""
2
-
3
- from url_normalize.url_normalize import normalize_path
4
-
5
- EXPECTED_DATA = {
6
- "..": "/",
7
- "": "/",
8
- "/../foo": "/foo",
9
- "/..foo": "/..foo",
10
- "/./../foo": "/foo",
11
- "/./foo": "/foo",
12
- "/./foo/.": "/foo/",
13
- "/.foo": "/.foo",
14
- "/": "/",
15
- "/foo..": "/foo..",
16
- "/foo.": "/foo.",
17
- "/FOO": "/FOO",
18
- "/foo/../bar": "/bar",
19
- "/foo/./bar": "/foo/bar",
20
- "/foo//": "/foo/",
21
- "/foo///bar//": "/foo/bar/",
22
- "/foo/bar/..": "/foo/",
23
- "/foo/bar/../..": "/",
24
- "/foo/bar/../../../../baz": "/baz",
25
- "/foo/bar/../../../baz": "/baz",
26
- "/foo/bar/../../": "/",
27
- "/foo/bar/../../baz": "/baz",
28
- "/foo/bar/../": "/foo/",
29
- "/foo/bar/../baz": "/foo/baz",
30
- "/foo/bar/.": "/foo/bar/",
31
- "/foo/bar/./": "/foo/bar/",
32
- # Issue #25: we should preserve ? in the path
33
- "/More+Tea+Vicar%3F/discussion": "/More+Tea+Vicar%3F/discussion",
34
- }
35
-
36
-
37
- def test_normalize_path_result_is_expected():
38
- """Assert we got expected results from the normalize_path function."""
39
- for url, expected in EXPECTED_DATA.items():
40
- result = normalize_path(url, "http")
41
-
42
- assert result == expected, url
@@ -1,13 +0,0 @@
1
- """Tests for normalize_port function."""
2
-
3
- from url_normalize.url_normalize import normalize_port
4
-
5
- EXPECTED_DATA = {"8080": "8080", "": "", "80": "", "string": "string"}
6
-
7
-
8
- def test_normalize_port_result_is_expected():
9
- """Assert we got expected results from the normalize_port function."""
10
- for url, expected in EXPECTED_DATA.items():
11
- result = normalize_port(url, "http")
12
-
13
- assert result == expected, url
@@ -1,13 +0,0 @@
1
- """Tests for normalize_scheme function."""
2
-
3
- from url_normalize.url_normalize import normalize_scheme
4
-
5
- EXPECTED_DATA = {"http": "http", "HTTP": "http"}
6
-
7
-
8
- def test_normalize_scheme_result_is_expected():
9
- """Assert we got expected results from the normalize_scheme function."""
10
- for url, expected in EXPECTED_DATA.items():
11
- result = normalize_scheme(url)
12
-
13
- assert result == expected, url
@@ -1,19 +0,0 @@
1
- """Tests for normalize_userinfo function."""
2
-
3
- from url_normalize.url_normalize import normalize_userinfo
4
-
5
- EXPECTED_DATA = {
6
- ":@": "",
7
- "": "",
8
- "@": "",
9
- "user:password@": "user:password@",
10
- "user@": "user@",
11
- }
12
-
13
-
14
- def test_normalize_userinfo_result_is_expected():
15
- """Assert we got expected results from the normalize_userinfo function."""
16
- for url, expected in EXPECTED_DATA.items():
17
- result = normalize_userinfo(url)
18
-
19
- assert result == expected, url
@@ -1,30 +0,0 @@
1
- """Tests for provide_url_scheme function."""
2
-
3
- from url_normalize.url_normalize import provide_url_scheme
4
-
5
- EXPECTED_DATA = {
6
- "": "",
7
- "-": "-",
8
- "/file/path": "/file/path",
9
- "//site/path": "https://site/path",
10
- "ftp://site/": "ftp://site/",
11
- "site/page": "https://site/page",
12
- }
13
-
14
-
15
- def test_provide_url_scheme_result_is_expected():
16
- """Assert we got expected results from the provide_url_scheme function."""
17
- for url, expected in EXPECTED_DATA.items():
18
- result = provide_url_scheme(url)
19
-
20
- assert result == expected, url
21
-
22
-
23
- def test_provide_url_scheme_accept_default_scheme_param():
24
- """Assert we could provide default_scheme param other than https."""
25
- url = "//site/path"
26
- expected = "http://site/path"
27
-
28
- actual = provide_url_scheme(url, default_scheme="http")
29
-
30
- assert actual == expected
@@ -1,41 +0,0 @@
1
- """Reconstruct url tests."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Final
6
-
7
- from url_normalize.tools import URL, reconstruct_url
8
-
9
- EXPECTED_DATA: Final[tuple[tuple[URL, str], ...]] = (
10
- (
11
- URL(
12
- fragment="",
13
- host="site.com",
14
- path="",
15
- port="",
16
- query="",
17
- scheme="http",
18
- userinfo="",
19
- ),
20
- "http://site.com",
21
- ),
22
- (
23
- URL(
24
- fragment="fragment",
25
- host="www.example.com",
26
- path="/path/index.html",
27
- port="8080",
28
- query="param=val",
29
- scheme="http",
30
- userinfo="user@",
31
- ),
32
- "http://user@www.example.com:8080/path/index.html?param=val#fragment",
33
- ),
34
- )
35
-
36
-
37
- def test_deconstruct_url_result_is_expected() -> None:
38
- """Assert we got expected results from the deconstruct_url function."""
39
- for url, expected in EXPECTED_DATA:
40
- result = reconstruct_url(url)
41
- assert result == expected, url
File without changes
File without changes