pathlib-next 0.4.0__tar.gz → 0.5.0__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.
Files changed (59) hide show
  1. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/.gitignore +3 -0
  2. pathlib_next-0.5.0/CHANGELOG.md +215 -0
  3. pathlib_next-0.5.0/PKG-INFO +203 -0
  4. pathlib_next-0.5.0/README.md +168 -0
  5. pathlib_next-0.5.0/docs/divergences.md +61 -0
  6. pathlib_next-0.5.0/docs/guides/extending.md +121 -0
  7. pathlib_next-0.5.0/docs/guides/schemes.md +39 -0
  8. pathlib_next-0.5.0/docs/index.md +85 -0
  9. pathlib_next-0.5.0/examples/http_listing.py +38 -0
  10. pathlib_next-0.5.0/examples/local_and_mem.py +63 -0
  11. pathlib_next-0.5.0/examples/sftp_sync.py +57 -0
  12. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/mkdocs.yml +4 -0
  13. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/pyproject.toml +2 -2
  14. pathlib_next-0.5.0/src/pathlib_next/fspath.py +135 -0
  15. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/mempath.py +67 -17
  16. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/path.py +148 -25
  17. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/protocols/fs.py +10 -5
  18. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/protocols/io.py +13 -4
  19. pathlib_next-0.5.0/src/pathlib_next/testing.py +132 -0
  20. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/__init__.py +64 -13
  21. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/query.py +41 -2
  22. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/schemes/file.py +7 -0
  23. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/schemes/http.py +34 -6
  24. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/schemes/sftp.py +26 -4
  25. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/source.py +11 -0
  26. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/utils/__init__.py +13 -5
  27. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/utils/glob.py +42 -4
  28. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/utils/stat.py +5 -0
  29. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/utils/sync.py +15 -0
  30. pathlib_next-0.5.0/tests/conftest.py +63 -0
  31. pathlib_next-0.5.0/tests/test_contract.py +29 -0
  32. pathlib_next-0.5.0/tests/test_glob.py +91 -0
  33. pathlib_next-0.5.0/tests/test_http.py +70 -0
  34. pathlib_next-0.5.0/tests/test_mempath.py +155 -0
  35. pathlib_next-0.5.0/tests/test_parity_io.py +159 -0
  36. pathlib_next-0.5.0/tests/test_parity_pure.py +102 -0
  37. pathlib_next-0.5.0/tests/test_pathname.py +136 -0
  38. pathlib_next-0.5.0/tests/test_query.py +45 -0
  39. pathlib_next-0.5.0/tests/test_sftp.py +158 -0
  40. pathlib_next-0.5.0/tests/test_smoke.py +95 -0
  41. pathlib_next-0.5.0/tests/test_source.py +62 -0
  42. pathlib_next-0.5.0/tests/test_sync.py +122 -0
  43. pathlib_next-0.4.0/tests/test_uri.py → pathlib_next-0.5.0/tests/test_uri_parse.py +47 -0
  44. pathlib_next-0.5.0/tests/test_uri_path.py +100 -0
  45. pathlib_next-0.5.0/tests/test_utils.py +68 -0
  46. pathlib_next-0.4.0/CHANGELOG.md +0 -31
  47. pathlib_next-0.4.0/PKG-INFO +0 -115
  48. pathlib_next-0.4.0/README.md +0 -81
  49. pathlib_next-0.4.0/docs/index.md +0 -29
  50. pathlib_next-0.4.0/examples/example.py +0 -70
  51. pathlib_next-0.4.0/src/pathlib_next/fspath.py +0 -85
  52. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/LICENSE +0 -0
  53. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/docs/api/reference.md +0 -0
  54. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/docs/changelog.md +0 -0
  55. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/__init__.py +0 -0
  56. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/protocols/__init__.py +0 -0
  57. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/py.typed +0 -0
  58. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/src/pathlib_next/uri/schemes/__init__.py +0 -0
  59. {pathlib_next-0.4.0 → pathlib_next-0.5.0}/tests/test_local.py +0 -0
@@ -16,6 +16,9 @@ __pycache__/
16
16
  .pytest_cache/
17
17
  .mypy_cache/
18
18
  .ruff_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
19
22
  *.egg-info/
20
23
  .venv/
21
24
 
@@ -0,0 +1,215 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-07-11
11
+
12
+ ### Fixed (critical -- found while writing Phase 6 examples)
13
+ - `Path("...")` -- the top-level dispatcher documented in this project's
14
+ own README quick start and used throughout -- silently dropped its
15
+ constructor arguments on Python <3.12, leaving a blank instance that
16
+ crashed with `AttributeError: _drv` the moment anything touched it (e.g.
17
+ the `/` operator). Masked on 3.12+, where the real parsing happens in
18
+ `__init__` (called separately, with the original args, regardless of what
19
+ `__new__` did) rather than `__new__` itself. Every one of Phase 5's 300
20
+ tests constructed via `LocalPath(...)` directly instead, so this went
21
+ undetected until `examples/local_and_mem.py` exercised the documented
22
+ `Path(...)` entry point end to end.
23
+
24
+ ### Fixed (found by the new Phase 5 test suite, not in the original bug list)
25
+ - `LocalPath.stat()`/`chmod()` inherit directly from `pathlib.Path` via MRO
26
+ and crashed with `TypeError` on Python 3.9 the moment anything passed
27
+ `follow_symlinks=` (e.g. `Path.walk()`'s default `follow_symlinks=False`)
28
+ -- now shimmed with `lstat()`/`lchmod()` on <3.10, same as the existing
29
+ `FileUri` shim (which now just delegates to `LocalPath`).
30
+ - `MemPath.__init__` decided whether to propagate a parent's backend with
31
+ `if _backend and backend is None:` -- an empty (but valid) backend dict is
32
+ falsy, so joining off a freshly-created, empty `MemPath` silently gave the
33
+ child a disconnected new backend instead of sharing the parent's.
34
+ - `MemPath.stat()` never set `st_size` for files (always defaulted to `0`),
35
+ breaking any size-based checksum comparison (notably `PathSyncer`'s
36
+ typical usage).
37
+ - `glob()`'s core algorithm decided whether to recurse into the *parent*
38
+ directory using whether the *leaf* segment is a wildcard, instead of
39
+ whether the *parent path itself* contains one. Since a wildcarded leaf
40
+ with a literal parent directory is the overwhelmingly common case
41
+ (`glob("*.py")`), this always took the "recurse into parent" branch,
42
+ which only degenerated back to the correct single directory when the
43
+ parent has a non-empty literal name to re-match against -- true for
44
+ essentially every real filesystem path except an OS root. It silently
45
+ returned the wrong result on `MemPath`'s virtual root (empty name).
46
+ - `HttpPath.iterdir()` gave every subdirectory entry an empty `.name`:
47
+ directory-listing entries for subdirectories carry a trailing `/`
48
+ (`htmllistparse`'s convention), which wasn't stripped before building the
49
+ child's path, and `Pathname.name` derives from the last path segment --
50
+ empty for a trailing-slash path.
51
+ - `SftpPath.rename()` resolved a plain string target relative to `self`
52
+ (joining it as a child, e.g. `"/a.txt".rename("b.txt")` produced
53
+ `"/a.txt/b.txt"`) instead of `self`'s parent (sibling rename).
54
+
55
+ ### Added (test suite)
56
+ - Full pytest suite (`tests/`): pure-path parity against `pathlib.PurePosixPath`
57
+ (`test_parity_pure.py`), local I/O parity against `pathlib.Path`/`os.walk`
58
+ (`test_parity_io.py`), a reusable filesystem-contract mixin run against
59
+ `LocalPath`/`MemPath`/`FileUri` and exported as `pathlib_next.testing.
60
+ PathContract` for third-party `Path`/`UriPath` implementers
61
+ (`test_contract.py`), glob vs. stdlib ground truth (`test_glob.py`), URI
62
+ parsing/scheme-dispatch/query/source coverage, MemPath- and SFTP-specific
63
+ unit tests (SFTP mocked, no real server), HTTP tests against a real stdlib
64
+ `ThreadingHTTPServer`, and `PathSyncer` coverage. 300 tests, ~85% line
65
+ coverage, green on both Python 3.9 and 3.13.
66
+
67
+ ### Added (docs)
68
+ - `docs/guides/schemes.md` (capability matrix per scheme) and
69
+ `docs/guides/extending.md` (both extension tracks, with worked examples
70
+ and `pathlib_next.testing.PathContract` usage). Rewrote `docs/index.md`
71
+ and the README with a 30-second example per scheme and a capability
72
+ matrix. Class-level docstrings added across the package for the rendered
73
+ API reference.
74
+
75
+ ### Changed
76
+ - `examples/example.py` (an unstructured scratch script) split into three
77
+ focused, runnable examples: `examples/local_and_mem.py` (self-contained,
78
+ no network), `examples/http_listing.py` and `examples/sftp_sync.py`
79
+ (network-touching, guarded under `if __name__ == "__main__"`,
80
+ configurable via env vars, fail soft when unreachable/unconfigured).
81
+
82
+ ### Added
83
+ - `Pathname.joinpath()`, `Pathname.full_match()` (3.13 parity, supports `**`
84
+ matching any number of segments), `Pathname.anchor`/`drive`/`root`
85
+ (generic derivation for non-local paths), `Path.rglob()`,
86
+ `read_text(..., newline=)` (3.13 parity), `Path.samefile()` (default
87
+ `st_dev`/`st_ino` comparison when the backend's `stat()` provides them,
88
+ `NotImplementedError` otherwise).
89
+ - `Path.glob()`/`LocalPath.glob()`: `recursive=` now auto-detects (`True` if
90
+ the pattern has a `"**"` component) instead of defaulting to `False`;
91
+ explicit `recursive=True`/`False` still overrides.
92
+ - `Path.copy()`: raises `IsADirectoryError` when the target is an existing
93
+ directory (previously misbehaved); gained `follow_symlinks=`/
94
+ `preserve_metadata=` kwargs, named to match CPython 3.14's `Path.copy()`.
95
+ - `docs/divergences.md`: registry of every deliberate behavioral divergence
96
+ from `pathlib`, with rationale. Linked from the docs nav.
97
+
98
+ ### Fixed
99
+ - `Path.mkdir(parents=True)` created intermediate parents with `exist_ok=False`
100
+ (racy, and wrong when a parent already existed) and dropped the caller's
101
+ `exist_ok` on the final retry.
102
+ - `Path.touch(exist_ok=False)` silently truncated an existing file instead of
103
+ raising `FileExistsError` (pathlib parity).
104
+ - `LocalPath.glob()`'s `dironly` parameter defaulted to `False`, which made the
105
+ `is None` check for trailing-slash directory-only detection dead code.
106
+ - `Stat._st_mode()` only caught `FileNotFoundError`, letting `PermissionError`
107
+ and other `OSError`s propagate out of `exists()`/`is_dir()`/etc. where pathlib
108
+ returns `False`. Also fixed: `follow_symlinks` was accepted but never
109
+ forwarded to the underlying `stat()` call, so `is_symlink()` never actually
110
+ inspected the symlink itself.
111
+ - `MemPath._open()` treated any mode other than `"w"` as a read, so `"a"`/`"x"`
112
+ silently misbehaved; now dispatches `r`/`w`/`x`/`a` correctly and raises
113
+ `NotImplementedError` for anything else. `MemBytesIO.close()` used
114
+ `seek(0);read()` instead of `getvalue()`, losing content if the caller's
115
+ cursor wasn't already at position 0 when closing.
116
+ - `MemPath.normalized` mangled `".."`-escaping paths (e.g. `".."`) into `"."`;
117
+ now normalizes against a virtual root so they clamp at the root instead.
118
+ - `PathAndStat.__getattr__()` returned `None` for any unrecognized attribute
119
+ instead of raising `AttributeError`, breaking `hasattr()`-based logic.
120
+ - `parsedate(None)` / an unparseable date string returned "now" instead of
121
+ epoch 0, which could poison `PathSyncer`'s checksum/freshness comparisons for
122
+ HTTP sources with no `Last-Modified` header.
123
+ - `HttpPath.stat()` used a bare `except:`; cached `_isdir` from a response that
124
+ hadn't been confirmed successful yet (including 404s); and didn't fall back
125
+ to GET when a server rejected `HEAD` with 405.
126
+ - `uri.Query` no longer depends on `uritools`' private `_querydict`/`_querylist`
127
+ helpers (reimplemented locally against the public `uriencode()`).
128
+ - `Uri` join (`_load_parts`): `query`/`fragment` are now resolved with the same
129
+ "last segment that actually sets one wins" rule already used for `source`
130
+ (previously any segment, even one with no query/fragment, would blank out an
131
+ earlier segment's). Join semantics are now documented explicitly:
132
+ pathlib-`joinpath`-like, not RFC 3986 reference resolution, `..` is never
133
+ resolved during join.
134
+ - `Source.is_local()` (DNS lookup) and `get_machine_ips()` are now
135
+ `functools.lru_cache`d -- previously ran on every call.
136
+
137
+ ### Fixed (crash-level bugs)
138
+ - `MemPath.stat()`/`MemPath._open()` returned a `FileNotFoundError` instance instead
139
+ of raising it for a missing path, causing an unrelated `AttributeError` downstream.
140
+ - `LRU.invalidate()` called `self.lock()` instead of using `self.lock` as a context
141
+ manager (`RLock` isn't callable) -- broke the SFTP client reconnect path.
142
+ - `Pathname.match()` had reversed `isinstance()` arguments and compared against
143
+ `str(self)` (which includes scheme/host for `Uri`) instead of `as_posix()`.
144
+ - Glob wildcard detection (`WILCARD_PATTERN`, renamed `WILDCARD_PATTERN`, old name
145
+ kept as an alias) used `.match()` (anchored) instead of `.search()`, so patterns
146
+ like `"foo*"` weren't recognized as wildcards.
147
+ - `Uri` was unhashable (defined `__eq__` without `__hash__`); `__eq__` now also
148
+ returns `NotImplemented` for non-`Pathname`/`str` operands instead of raising.
149
+ - `Uri.is_relative_to()` used `str.startswith()` on normalized path strings, so
150
+ `/foo/bar2` was incorrectly reported as relative to `/foo/bar`; now compares
151
+ path segments.
152
+ - `Uri.relative_to(walk_up=True)` was dead code -- an early guard raised
153
+ `ValueError` before the walk-up loop ever ran.
154
+ - `HttpPath.is_dir()`/`is_file()` tested truthiness of bound methods
155
+ (`self._is_dir`, `self.is_dir`) instead of calling/checking the right attribute,
156
+ so both always returned truthy nonsense.
157
+ - `SftpPath.chmod()` didn't accept `follow_symlinks=`, so the inherited `lchmod()`
158
+ crashed with `TypeError`; now raises `NotImplementedError` for
159
+ `follow_symlinks=False` (paramiko has no `lchmod`).
160
+ - `SftpPath` defined `_rename()`, which nothing ever called -- renamed to
161
+ `rename()` so `move()`/`rename()` actually use SFTP's native rename instead of
162
+ silently falling back to copy+unlink for every move.
163
+ - `Uri.__init__()` used a bare `except:` around `Path.as_uri()` (now
164
+ `except ValueError:`, matching what `as_uri()` actually raises for relative
165
+ paths) and crashed with `AttributeError` when constructing from an
166
+ `os.PathLike` that only implements `__fspath__` (no `as_posix()`).
167
+ - `Path.rm(ignore_error=callable)` never actually called the callable -- both
168
+ branches of its error handler returned the callable object itself.
169
+
170
+ ### Fixed (Python 3.9/3.10 compatibility)
171
+ - Actual Python 3.9/3.10 runtime compatibility (CI previously only tested 3.11/3.13
172
+ and missed these): `LocalPath`/`Uri` case-sensitivity and path-separator detection
173
+ crashed on 3.9-3.11 (`_flavour` object has no `normcase`); `open(mode="r")` crashed
174
+ on <3.10 (`io.text_encoding` is 3.10+); glob pattern compilation crashed on <3.11
175
+ (`re.NOFLAG` is 3.11+); `FileUri.stat()`/`chmod()` crashed on 3.9
176
+ (`pathlib.Path.stat/chmod` gained `follow_symlinks=` in 3.10; raises
177
+ `NotImplementedError` there for `follow_symlinks=False`).
178
+ - `LocalPath._path_separators` returned the env-var list separator (`;`/`:`) instead
179
+ of the path separator, and could include a `None` altsep on POSIX.
180
+
181
+ ### Added
182
+ - `tests/test_smoke.py`: regression coverage for README/example snippets across
183
+ supported Python versions.
184
+
185
+ ## [0.4.1] - 2026-07-11
186
+
187
+ ### Fixed
188
+ - Removed explicit `[tool.hatch.build.targets.wheel]` packages config that caused hatchling to fail resolving `README.md` during editable installs on CI.
189
+ - Converted `README.md` from a symlink (mode `120000`) to a regular file, fixing `git checkout` failures on macOS and Windows runners.
190
+ - Removed agent-tooling references (`AGENTS.md`, `PYTHON.md`) from committed files.
191
+
192
+ ## [0.4.0] - 2026-07-11
193
+
194
+ ### Added
195
+ - Standardized repository layout and relocated examples to `examples/` directory.
196
+ - Configured MkDocs documentation site with dynamic API reference using `mkdocstrings`.
197
+ - Added GitHub Actions workflows for matrix testing (`test.yml`) and release pipelines (`release.yml`).
198
+ - Added typing marker `py.typed` for PEP 561 compliance.
199
+
200
+ ### Changed
201
+ - Added backward compatibility support for Python 3.9 and 3.10: added `from __future__ import annotations` across the codebase, refactored runtime-evaluated union types to use `typing.Union`, and provided fallbacks for `TypeAlias` and `ParamSpec`.
202
+ - Updated package requirement to `requires-python = ">=3.9"`.
203
+
204
+ ## [0.3.5] - 2026-07-11
205
+
206
+ ### Added
207
+ - Split path into protocols that can be standalone.
208
+ - Sync error handling.
209
+ - Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
210
+
211
+ [Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.5.0...HEAD
212
+ [0.5.0]: https://github.com/jose-pr/pathlib_next/compare/v0.4.1...v0.5.0
213
+ [0.4.1]: https://github.com/jose-pr/pathlib_next/compare/v0.4.0...v0.4.1
214
+ [0.4.0]: https://github.com/jose-pr/pathlib_next/releases/tag/v0.4.0
215
+ [0.3.5]: https://github.com/jose-pr/pathlib_next/releases/tag/v0.3.5
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: pathlib_next
3
+ Version: 0.5.0
4
+ Summary: Generic Path Protocol based pathlib
5
+ Project-URL: Homepage, https://github.com/jose-pr/pathlib_next/
6
+ Project-URL: Documentation, https://jose-pr.github.io/pathlib_next/
7
+ Project-URL: Issues, https://github.com/jose-pr/pathlib_next/issues
8
+ Author: Jose A
9
+ License-File: LICENSE
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.9
14
+ Provides-Extra: dev
15
+ Requires-Dist: build; extra == 'dev'
16
+ Requires-Dist: hatchling; extra == 'dev'
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Requires-Dist: pytest-cov; extra == 'dev'
19
+ Requires-Dist: twine; extra == 'dev'
20
+ Provides-Extra: docs
21
+ Requires-Dist: mkdocs; extra == 'docs'
22
+ Requires-Dist: mkdocs-material; extra == 'docs'
23
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
24
+ Provides-Extra: http
25
+ Requires-Dist: bs4; extra == 'http'
26
+ Requires-Dist: htmllistparse; extra == 'http'
27
+ Requires-Dist: requests; extra == 'http'
28
+ Requires-Dist: uritools; extra == 'http'
29
+ Provides-Extra: sftp
30
+ Requires-Dist: paramiko; extra == 'sftp'
31
+ Requires-Dist: uritools; extra == 'sftp'
32
+ Provides-Extra: uri
33
+ Requires-Dist: uritools; extra == 'uri'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # pathlib_next
37
+
38
+ [![Version](https://img.shields.io/pypi/v/pathlib_next.svg)](https://pypi.org/project/pathlib_next/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/pathlib_next.svg)](https://pypi.org/project/pathlib_next/)
40
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
41
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/pathlib_next/)
42
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/pathlib_next/test.yml)](https://github.com/jose-pr/pathlib_next/actions/workflows/test.yml)
43
+
44
+ A **robust, extensible pathlib-like base** for any resource addressable as a
45
+ path or URI. Same method names, signatures, semantics, and exception types as
46
+ `pathlib.Path` wherever a `pathlib.Path` equivalent exists -- write code once
47
+ against `Path`/`UriPath` and it works against your local disk, an in-memory
48
+ tree, an HTTP index, or an SFTP server. Every intentional divergence from
49
+ `pathlib`'s behavior is documented, not silent -- see
50
+ [`docs/divergences.md`](https://jose-pr.github.io/pathlib_next/divergences/).
51
+
52
+ ## Features
53
+
54
+ | Capability | `LocalPath` | `file:` | `mem:` (`MemPath`) | `http(s):` | `sftp:` |
55
+ | --- | --- | --- | --- | --- | --- |
56
+ | Read | Yes | Yes | Yes | Yes | Yes |
57
+ | Write | Yes | Yes | Yes | No | Yes |
58
+ | List (`iterdir`) | Yes | Yes | Yes | Yes (HTML index) | Yes |
59
+ | Stat / exists / is_dir / is_file | Yes | Yes | Yes | Yes | Yes |
60
+ | `mkdir` | Yes | Yes | Yes | No | Yes |
61
+ | Delete | Yes | Yes | Yes | No | Yes |
62
+ | `rename` | Yes | Yes | No (copy+unlink fallback) | No | Yes |
63
+ | Extra required | none | none | none | `http` | `sftp` |
64
+
65
+ Every scheme shares the same `glob()`, `walk()`, `copy()`/`move()`, `rm()`
66
+ implementations -- see the full matrix and notes in
67
+ [Schemes](https://jose-pr.github.io/pathlib_next/guides/schemes/).
68
+
69
+ - **Unified path interface** across local files, in-memory paths, and
70
+ `sftp`/`http`/`file` URIs.
71
+ - **`MemPath`** -- a lightweight virtual filesystem for mocks, tests, or
72
+ transient storage.
73
+ - **`PathSyncer`** -- one-way checksum-driven tree sync between any two
74
+ `Path` implementations, with dry-run and event hooks.
75
+ - **`Query`/`Source`** -- parse and serialize URL query strings and URI
76
+ authority components.
77
+ - **Extensible two ways**: subclass `Path` directly for a custom
78
+ non-URI resource, or subclass `UriPath` for a new URI scheme -- see
79
+ [Extending](https://jose-pr.github.io/pathlib_next/guides/extending/).
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install pathlib_next
85
+ ```
86
+
87
+ Optional features/extras:
88
+
89
+ | Extra/flag | Adds | Needed for |
90
+ | --- | --- | --- |
91
+ | `uri` | `uritools` | URI parsing capabilities |
92
+ | `http` | `requests`, `bs4`, `htmllistparse` | Read and list files over HTTP/HTTPS |
93
+ | `sftp` | `paramiko` | SFTP path operations and transfers |
94
+
95
+ `import pathlib_next` and `LocalPath`/`MemPath` work with no extras
96
+ installed.
97
+
98
+ ## Quick start
99
+
100
+ **Local filesystem** -- drop-in `pathlib.Path`:
101
+
102
+ ```python
103
+ from pathlib_next import Path
104
+
105
+ p = Path("./data") / "report.txt"
106
+ p.write_text("hello")
107
+ print(p.read_text())
108
+ ```
109
+
110
+ **In-memory** (`mem:`) -- a virtual filesystem, no disk I/O:
111
+
112
+ ```python
113
+ from pathlib_next.mempath import MemPath
114
+
115
+ p = MemPath("/config/settings.json")
116
+ p.parent.mkdir(parents=True, exist_ok=True)
117
+ p.write_text('{"debug": true}')
118
+ ```
119
+
120
+ **`file:`** -- the same local filesystem, addressed as a URI:
121
+
122
+ ```python
123
+ from pathlib_next.uri import UriPath
124
+
125
+ p = UriPath("file:./data/report.txt")
126
+ print(p.read_text())
127
+ ```
128
+
129
+ **`http(s):`** -- read files and list Apache/nginx-style directory indexes:
130
+
131
+ ```python
132
+ from pathlib_next.uri import UriPath
133
+
134
+ p = UriPath("http://example.com/data/")
135
+ for child in p.iterdir():
136
+ if child.is_file():
137
+ print(child.name, child.stat().st_size)
138
+ ```
139
+
140
+ **`sftp:`** -- same interface, over SSH:
141
+
142
+ ```python
143
+ from pathlib_next.uri import UriPath
144
+
145
+ p = UriPath("sftp://user@host/var/log/app.log")
146
+ print(p.read_text())
147
+ ```
148
+
149
+ ## Extending
150
+
151
+ Two first-class ways to add a new path-addressable resource -- both covered
152
+ in depth, with worked examples, in
153
+ [Extending](https://jose-pr.github.io/pathlib_next/guides/extending/):
154
+
155
+ - Subclass `Path` directly for a custom, non-URI resource (`MemPath` is the
156
+ reference exemplar).
157
+ - Subclass `UriPath` and set `__SCHEMES` for a new URI scheme (`FileUri`/
158
+ `HttpPath`/`SftpPath` are the built-in examples).
159
+
160
+ `pathlib_next.testing.PathContract` is a reusable pytest mixin covering the
161
+ baseline contract every implementation must satisfy -- subclass it with a
162
+ `root` fixture to verify your own.
163
+
164
+ ## API overview
165
+
166
+ | Module/Package | Purpose |
167
+ | --- | --- |
168
+ | `pathlib_next.path` | Base Path implementation and protocols |
169
+ | `pathlib_next.uri` | URI/URL specific path support and Query utils |
170
+ | `pathlib_next.mempath` | In-memory transient path structure |
171
+ | `pathlib_next.utils.sync` | Synchronization functions and PathSyncer class |
172
+ | `pathlib_next.testing` | `PathContract`, a pytest mixin for verifying custom implementations |
173
+
174
+ ## Supported Python versions
175
+
176
+ Python >= 3.9, tested on 3.9 and 3.13 in CI (see
177
+ [`.github/workflows/test.yml`](.github/workflows/test.yml)).
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ pip install -e ".[dev,uri,http,sftp]"
183
+ pytest -q
184
+ ```
185
+
186
+ If you maintain separate virtual environments per Python version locally
187
+ (e.g. `.venv/3.9/`, `.venv/3.13/`), run the same `pytest -q` in each --
188
+ CI does the equivalent across Python 3.9/3.13 on Linux, macOS, and Windows.
189
+
190
+ ### Releasing
191
+
192
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
193
+ [`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
194
+ workflow: test gate → build → publish → docs deploy.
195
+
196
+ ### Documentation site
197
+
198
+ MkDocs builds the API reference from `docs/`, published on every
199
+ release. To preview locally: `mkdocs serve`.
200
+
201
+ ## License
202
+
203
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,168 @@
1
+ # pathlib_next
2
+
3
+ [![Version](https://img.shields.io/pypi/v/pathlib_next.svg)](https://pypi.org/project/pathlib_next/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/pathlib_next.svg)](https://pypi.org/project/pathlib_next/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/pathlib_next/)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/pathlib_next/test.yml)](https://github.com/jose-pr/pathlib_next/actions/workflows/test.yml)
8
+
9
+ A **robust, extensible pathlib-like base** for any resource addressable as a
10
+ path or URI. Same method names, signatures, semantics, and exception types as
11
+ `pathlib.Path` wherever a `pathlib.Path` equivalent exists -- write code once
12
+ against `Path`/`UriPath` and it works against your local disk, an in-memory
13
+ tree, an HTTP index, or an SFTP server. Every intentional divergence from
14
+ `pathlib`'s behavior is documented, not silent -- see
15
+ [`docs/divergences.md`](https://jose-pr.github.io/pathlib_next/divergences/).
16
+
17
+ ## Features
18
+
19
+ | Capability | `LocalPath` | `file:` | `mem:` (`MemPath`) | `http(s):` | `sftp:` |
20
+ | --- | --- | --- | --- | --- | --- |
21
+ | Read | Yes | Yes | Yes | Yes | Yes |
22
+ | Write | Yes | Yes | Yes | No | Yes |
23
+ | List (`iterdir`) | Yes | Yes | Yes | Yes (HTML index) | Yes |
24
+ | Stat / exists / is_dir / is_file | Yes | Yes | Yes | Yes | Yes |
25
+ | `mkdir` | Yes | Yes | Yes | No | Yes |
26
+ | Delete | Yes | Yes | Yes | No | Yes |
27
+ | `rename` | Yes | Yes | No (copy+unlink fallback) | No | Yes |
28
+ | Extra required | none | none | none | `http` | `sftp` |
29
+
30
+ Every scheme shares the same `glob()`, `walk()`, `copy()`/`move()`, `rm()`
31
+ implementations -- see the full matrix and notes in
32
+ [Schemes](https://jose-pr.github.io/pathlib_next/guides/schemes/).
33
+
34
+ - **Unified path interface** across local files, in-memory paths, and
35
+ `sftp`/`http`/`file` URIs.
36
+ - **`MemPath`** -- a lightweight virtual filesystem for mocks, tests, or
37
+ transient storage.
38
+ - **`PathSyncer`** -- one-way checksum-driven tree sync between any two
39
+ `Path` implementations, with dry-run and event hooks.
40
+ - **`Query`/`Source`** -- parse and serialize URL query strings and URI
41
+ authority components.
42
+ - **Extensible two ways**: subclass `Path` directly for a custom
43
+ non-URI resource, or subclass `UriPath` for a new URI scheme -- see
44
+ [Extending](https://jose-pr.github.io/pathlib_next/guides/extending/).
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install pathlib_next
50
+ ```
51
+
52
+ Optional features/extras:
53
+
54
+ | Extra/flag | Adds | Needed for |
55
+ | --- | --- | --- |
56
+ | `uri` | `uritools` | URI parsing capabilities |
57
+ | `http` | `requests`, `bs4`, `htmllistparse` | Read and list files over HTTP/HTTPS |
58
+ | `sftp` | `paramiko` | SFTP path operations and transfers |
59
+
60
+ `import pathlib_next` and `LocalPath`/`MemPath` work with no extras
61
+ installed.
62
+
63
+ ## Quick start
64
+
65
+ **Local filesystem** -- drop-in `pathlib.Path`:
66
+
67
+ ```python
68
+ from pathlib_next import Path
69
+
70
+ p = Path("./data") / "report.txt"
71
+ p.write_text("hello")
72
+ print(p.read_text())
73
+ ```
74
+
75
+ **In-memory** (`mem:`) -- a virtual filesystem, no disk I/O:
76
+
77
+ ```python
78
+ from pathlib_next.mempath import MemPath
79
+
80
+ p = MemPath("/config/settings.json")
81
+ p.parent.mkdir(parents=True, exist_ok=True)
82
+ p.write_text('{"debug": true}')
83
+ ```
84
+
85
+ **`file:`** -- the same local filesystem, addressed as a URI:
86
+
87
+ ```python
88
+ from pathlib_next.uri import UriPath
89
+
90
+ p = UriPath("file:./data/report.txt")
91
+ print(p.read_text())
92
+ ```
93
+
94
+ **`http(s):`** -- read files and list Apache/nginx-style directory indexes:
95
+
96
+ ```python
97
+ from pathlib_next.uri import UriPath
98
+
99
+ p = UriPath("http://example.com/data/")
100
+ for child in p.iterdir():
101
+ if child.is_file():
102
+ print(child.name, child.stat().st_size)
103
+ ```
104
+
105
+ **`sftp:`** -- same interface, over SSH:
106
+
107
+ ```python
108
+ from pathlib_next.uri import UriPath
109
+
110
+ p = UriPath("sftp://user@host/var/log/app.log")
111
+ print(p.read_text())
112
+ ```
113
+
114
+ ## Extending
115
+
116
+ Two first-class ways to add a new path-addressable resource -- both covered
117
+ in depth, with worked examples, in
118
+ [Extending](https://jose-pr.github.io/pathlib_next/guides/extending/):
119
+
120
+ - Subclass `Path` directly for a custom, non-URI resource (`MemPath` is the
121
+ reference exemplar).
122
+ - Subclass `UriPath` and set `__SCHEMES` for a new URI scheme (`FileUri`/
123
+ `HttpPath`/`SftpPath` are the built-in examples).
124
+
125
+ `pathlib_next.testing.PathContract` is a reusable pytest mixin covering the
126
+ baseline contract every implementation must satisfy -- subclass it with a
127
+ `root` fixture to verify your own.
128
+
129
+ ## API overview
130
+
131
+ | Module/Package | Purpose |
132
+ | --- | --- |
133
+ | `pathlib_next.path` | Base Path implementation and protocols |
134
+ | `pathlib_next.uri` | URI/URL specific path support and Query utils |
135
+ | `pathlib_next.mempath` | In-memory transient path structure |
136
+ | `pathlib_next.utils.sync` | Synchronization functions and PathSyncer class |
137
+ | `pathlib_next.testing` | `PathContract`, a pytest mixin for verifying custom implementations |
138
+
139
+ ## Supported Python versions
140
+
141
+ Python >= 3.9, tested on 3.9 and 3.13 in CI (see
142
+ [`.github/workflows/test.yml`](.github/workflows/test.yml)).
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ pip install -e ".[dev,uri,http,sftp]"
148
+ pytest -q
149
+ ```
150
+
151
+ If you maintain separate virtual environments per Python version locally
152
+ (e.g. `.venv/3.9/`, `.venv/3.13/`), run the same `pytest -q` in each --
153
+ CI does the equivalent across Python 3.9/3.13 on Linux, macOS, and Windows.
154
+
155
+ ### Releasing
156
+
157
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
158
+ [`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
159
+ workflow: test gate → build → publish → docs deploy.
160
+
161
+ ### Documentation site
162
+
163
+ MkDocs builds the API reference from `docs/`, published on every
164
+ release. To preview locally: `mkdocs serve`.
165
+
166
+ ## License
167
+
168
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,61 @@
1
+ # Divergences from `pathlib`
2
+
3
+ `pathlib_next` targets `pathlib.Path` parity: same method names, signatures,
4
+ semantics and exception types wherever a `pathlib.Path` equivalent exists.
5
+ Extensions (extra optional kwargs, new methods like `rm()`/`sync`) are
6
+ allowed. Any *behavioral* divergence from `pathlib` on a method that exists
7
+ in both must be listed here -- no silent divergence.
8
+
9
+ `LocalPath` is `pathlib.WindowsPath`/`pathlib.PosixPath` with our `Path` mixed
10
+ in via MRO, so unless noted otherwise it behaves exactly like `pathlib.Path`
11
+ (it inherits the real implementation for anything not explicitly overridden).
12
+ The divergences below apply to `Uri`/`UriPath` and `MemPath`.
13
+
14
+ | Method | pathlib behavior | Our behavior | Why |
15
+ | --- | --- | --- | --- |
16
+ | `Uri("a").parent` | `PurePosixPath("a").parent == PurePosixPath(".")` | `Uri("a").parent` has path `""` (`Uri("")`, which round-trips) | `Uri` has no cwd-relative concept of `"."` -- an empty path is the URI-natural "no path" representation. Changing this would make `Uri("")` non-idempotent under `.parent`. |
17
+ | `with_name()` / `with_suffix()` / `with_stem()` on `Uri`/`UriPath` | N/A (pathlib has no query/fragment) | Preserve the URI's query and fragment (implemented via `with_path`, which carries them over) | Deliberate extension: `UriPath("http://h/a?x=1").with_suffix(".txt")` keeping `?x=1` matches how most callers actually want to retarget just the path component of a URL. **User decision, 2026-07-11.** |
18
+ | `Path.__iter__` | `pathlib.Path` is not iterable (no `__iter__`) | `iter(path)` is `path.iterdir()` | Deliberate extension for ergonomic `for child in path:` loops. **Caution:** on remote schemes (http/sftp) this is a network call. **User decision, 2026-07-11.** |
19
+ | `Path.copy(target, ...)` | CPython 3.14 `Path.copy(target, *, follow_symlinks=True, dirs_exist_ok=False, preserve_metadata=False)`; always raises if `target` exists | Ours predates 3.14. Signature: `copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True)`. `overwrite=True` unlinks an existing non-directory target first; `preserve_metadata` defaults to **True** (opposite of 3.14) and only propagates `st_mode`, not timestamps/xattrs | Argument names aligned with 3.14 where cheap; `preserve_metadata=True` default kept for backward compat with this method's pre-existing (pre-3.14-alignment) behavior of always copying the mode bits. Full metadata preservation (timestamps, xattrs) is not implemented. |
20
+ | `Path.move(target, ...)` | Not in `pathlib` at all | Our own extension: tries `rename()`, falls back to copy+unlink | N/A -- pure extension, no pathlib method to diverge from. |
21
+ | `Path.rm(recursive=, missing_ok=, ignore_error=)` | Not in `pathlib` (closest: `shutil.rmtree`) | Our own extension | N/A -- pure extension. |
22
+ | `PathSyncer` / `Query` / `Source` | N/A | Our own extensions | N/A -- pure extensions, no pathlib equivalent. |
23
+
24
+ ## Explicitly out of scope (not implemented on `Pathname`/`Path`)
25
+
26
+ These `pathlib.Path` methods are **not** part of the generic `Pathname`/`Path`
27
+ contract because they don't have a portable meaning across arbitrary
28
+ URI/virtual backends (a `MemPath` or `http://` URL has no filesystem-relative
29
+ cwd, no symlinks, no OS-level owner/group). `LocalPath` gets every one of
30
+ these for free from `pathlib.Path` via MRO -- this list only describes what
31
+ `Uri`/`UriPath`/`MemPath` (and custom `Path` subclasses in general) don't get:
32
+
33
+ - `resolve()`, `absolute()` -- no portable notion of "the current working
34
+ directory" or canonicalizing `..`/symlinks for an arbitrary backend.
35
+ - `readlink()`, `symlink_to()`, `hardlink_to()` -- no portable symlink/hardlink
36
+ concept (sftp *could* support these via paramiko, but it's not implemented).
37
+ - `owner()`, `group()` -- no portable uid/gid-to-name mapping.
38
+ - `expanduser()`, `Path.cwd()`, `Path.home()` -- inherently tied to the local
39
+ OS/filesystem, meaningless for a URI or in-memory path.
40
+ - `walk(..., follow_symlinks=True)` symlink-cycle protection -- `walk()`
41
+ itself is implemented (see `Path.walk`), but cycle detection when following
42
+ symlinks is not; only `LocalPath` (via pathlib) protects against symlink
43
+ loops during a followed walk.
44
+
45
+ ## Deliberate extensions (new methods/kwargs, not divergences)
46
+
47
+ These don't diverge from any existing pathlib behavior (pathlib has no
48
+ equivalent, or the kwarg is new/optional) -- listed for completeness, not
49
+ because a behavioral decision needed documenting:
50
+
51
+ - `joinpath(*args)`, `rglob(pattern)`, `full_match(pattern)` (3.13 parity),
52
+ `anchor`/`drive`/`root` on `Pathname` (generic derivation: `root` is `"/"`
53
+ when the first segment is empty, else `""`; `drive` is always `""`),
54
+ `read_text(..., newline=)` (3.13 parity) -- all additive, no divergence.
55
+ - `Path.glob()`/`LocalPath.glob()`: `recursive=` defaults to auto-detect
56
+ (`True` if the pattern contains a `"**"` component, else `False`) instead
57
+ of pathlib's implicit-always-recursive-on-`**` with no override. Passing
58
+ `recursive=False`/`True` explicitly always wins over the auto-detect.
59
+ **User decision, 2026-07-11.** `include_hidden=`/`dironly=` are documented
60
+ extensions beyond pathlib's `glob()` signature. **Caution:** on remote
61
+ schemes (http/sftp), a recursive glob walks the whole remote subtree.