Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated GitHub Actions workflows to use modern actions and UV package manager
- Modernized code formatting (improved consistency and readability)
- Centralized all package metadata in `pyproject.toml` (removed from module docstring)
=- **Updated all docstrings from reStructuredText to Google/NumPy style** (Args/Returns/Raises format)

### Added
- `.python-version` file for Python version management
Expand All @@ -41,6 +42,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CHANGELOG.md file (this file)
- `.github/copilot-instructions.md` for AI-assisted development
- `uv.lock` for reproducible dependency resolution
- **`from __future__ import annotations`** for modern type annotation support in Python 3.9+
- **Complete type annotations** for all 56+ functions, methods, and properties
- **Comprehensive docstrings** for all public classes, functions, methods, and properties
- **Pydocstyle linting** (Ruff "D" rules) to enforce documentation consistency
- **Type annotations for all test functions** (`-> None` return types)
- **Docstrings for magic methods** (`__str__`, `__bytes__`, etc.)

### Removed
- `setup.py` (replaced by `pyproject.toml`)
Expand Down
15 changes: 12 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,20 @@ select = [
"C4", # flake8-comprehensions
"PIE", # flake8-pie
"SIM", # flake8-simplify
"D", # pydocstyle (docstring linting)
]
ignore = [
"E501", # line too long (handled by formatter)
"D203", # one-blank-line-before-class (conflicts with D211)
"D213", # multi-line-summary-second-line (conflicts with D212)
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["D"] # Don't require docstrings in tests

[tool.ruff.lint.pydocstyle]
convention = "google" # Use Google-style docstrings

[tool.ruff.lint.isort]
known-first-party = ["urlpath"]

Expand All @@ -101,9 +110,9 @@ skip-magic-trailing-comma = false
python_version = "3.9"
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = false
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = false
no_implicit_optional = true
warn_redundant_casts = true
Expand Down
38 changes: 19 additions & 19 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from urlpath import URL, JailedURL


def test_simple():
def test_simple() -> None:
original = "http://www.example.com/path/to/file.ext?query#fragment"
url = URL(original)

Expand All @@ -32,7 +32,7 @@ def test_simple():
assert url.fragment == "fragment"


def test_netloc_mixin():
def test_netloc_mixin() -> None:
url = URL("https://username:[email protected]:1234/secure/path?query#fragment")

assert url.drive == "https://username:[email protected]:1234"
Expand All @@ -44,21 +44,21 @@ def test_netloc_mixin():
assert url.port == 1234


def test_join():
def test_join() -> None:
url = URL("http://www.example.com/path/to/file.ext?query#fragment")

assert str(url / "https://secure.example.com/path") == "https://secure.example.com/path"
assert str(url / "/changed/path") == "http://www.example.com/changed/path"
assert str(url.with_name("other_file")) == "http://www.example.com/path/to/other_file"


def test_path():
def test_path() -> None:
url = URL("http://www.example.com/path/to/file.ext?query#fragment")

assert url.path == "/path/to/file.ext"


def test_with():
def test_with() -> None:
url = URL("http://www.example.com/path/to/file.exe?query?fragment")

assert str(url.with_scheme("https")) == "https://www.example.com/path/to/file.exe?query?fragment"
Expand All @@ -78,7 +78,7 @@ def test_with():
)


def test_query():
def test_query() -> None:
query = "field1=value1&field1=value2&field2=hello,%20world%26python"
url = URL("http://www.example.com/form?" + query)

Expand All @@ -101,7 +101,7 @@ def test_query():
assert url.form.get("field4") == ("1", "2", "3")


def test_add_query():
def test_add_query() -> None:
query = "field1=value1&field1=value2&field2=hello,%20world%26python"
url = URL("http://www.example.com/form?" + query)

Expand Down Expand Up @@ -132,13 +132,13 @@ def test_add_query():
assert url.add_query({}).query == query


def test_query_field_order():
def test_query_field_order() -> None:
url = URL("http://example.com/").with_query(field1="field1", field2="field2", field3="field3")

assert str(url) == "http://example.com/?field1=field1&field2=field2&field3=field3"


def test_fragment():
def test_fragment() -> None:
url = URL("http://www.example.com/path/to/file.ext?query#fragment")

assert url.fragment == "fragment"
Expand All @@ -149,12 +149,12 @@ def test_fragment():
assert url.fragment == "new fragment"


def test_resolve():
def test_resolve() -> None:
url = URL("http://www.example.com//./../path/./..//./file/")
assert str(url.resolve()) == "http://www.example.com/file"


def test_trailing_sep():
def test_trailing_sep() -> None:
original = "http://www.example.com/path/with/trailing/sep/"
url = URL(original)

Expand All @@ -169,7 +169,7 @@ def test_trailing_sep():


@pytest.mark.skipif(webob is None, reason="webob not installed")
def test_webob():
def test_webob() -> None:
base_url = "http://www.example.com"
url = URL(webob.Request.blank("/webob/request", base_url=base_url))

Expand All @@ -179,7 +179,7 @@ def test_webob():


@pytest.mark.skipif(webob is None, reason="webob not installed")
def test_webob_jail():
def test_webob_jail() -> None:
request = webob.Request.blank("/path/to/filename.ext", {"SCRIPT_NAME": "/app/root"})

assert request.application_url == "http://localhost/app/root"
Expand All @@ -191,7 +191,7 @@ def test_webob_jail():
assert str(url) == "http://localhost/app/root/path/to/filename.ext"


def test_jail():
def test_jail() -> None:
root = "http://www.example.com/app/"
current = "http://www.example.com/app/path/to/content"
url = URL(root).jailed / current
Expand All @@ -208,13 +208,13 @@ def test_jail():
assert str(url / "http://www.example.com/app/path") == "http://www.example.com/app/path"


def test_init_with_empty_string():
def test_init_with_empty_string() -> None:
url = URL("")

assert str(url) == ""


def test_encoding():
def test_encoding() -> None:
assert URL("http://www.xn--alliancefranaise-npb.nu/").hostname == "www.alliancefran\xe7aise.nu"
assert (
str(URL("http://localhost/").with_hostinfo("www.alliancefran\xe7aise.nu"))
Expand Down Expand Up @@ -265,7 +265,7 @@ def test_encoding():
assert str(URL("http://example.com/file").with_suffix(".///")) == "http://example.com/file.%2F%2F%2F"


def test_idempotent():
def test_idempotent() -> None:
url = URL(
"http://\u65e5\u672c\u8a9e\u306e.\u30c9\u30e1\u30a4\u30f3.jp/"
"path/to/\u30d5\u30a1\u30a4\u30eb.ext?\u30af\u30a8\u30ea"
Expand All @@ -277,11 +277,11 @@ def test_idempotent():
)


def test_embed():
def test_embed() -> None:
url = URL("http://example.com/").with_fragment(URL("/param1/param2").with_query(f1=1, f2=2))
assert str(url) == "http://example.com/#/param1/param2?f1=1&f2=2"


def test_pchar():
def test_pchar() -> None:
url = URL("s3://mybucket") / "some_folder/123_2017-10-30T18:43:11.csv.gz"
assert str(url) == "s3://mybucket/some_folder/123_2017-10-30T18:43:11.csv.gz"
Loading