pathlib-next 0.4.1__tar.gz → 0.6.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 (65) hide show
  1. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/.gitignore +3 -0
  2. pathlib_next-0.6.0/CHANGELOG.md +243 -0
  3. pathlib_next-0.6.0/PKG-INFO +203 -0
  4. pathlib_next-0.6.0/README.md +168 -0
  5. pathlib_next-0.6.0/docs/divergences.md +61 -0
  6. pathlib_next-0.6.0/docs/guides/extending.md +121 -0
  7. pathlib_next-0.6.0/docs/guides/schemes.md +62 -0
  8. pathlib_next-0.6.0/docs/index.md +85 -0
  9. pathlib_next-0.6.0/examples/http_listing.py +38 -0
  10. pathlib_next-0.6.0/examples/local_and_mem.py +63 -0
  11. pathlib_next-0.6.0/examples/sftp_sync.py +57 -0
  12. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/mkdocs.yml +4 -0
  13. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/pyproject.toml +2 -2
  14. pathlib_next-0.6.0/src/pathlib_next/fspath.py +135 -0
  15. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/mempath.py +67 -17
  16. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/path.py +148 -25
  17. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/fs.py +10 -5
  18. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/io.py +13 -4
  19. pathlib_next-0.6.0/src/pathlib_next/testing.py +132 -0
  20. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/__init__.py +64 -13
  21. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/query.py +41 -2
  22. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/__init__.py +4 -0
  23. pathlib_next-0.6.0/src/pathlib_next/uri/schemes/archive.py +266 -0
  24. pathlib_next-0.6.0/src/pathlib_next/uri/schemes/data.py +58 -0
  25. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/file.py +7 -0
  26. pathlib_next-0.6.0/src/pathlib_next/uri/schemes/ftp.py +205 -0
  27. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/http.py +34 -6
  28. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/sftp.py +26 -4
  29. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/source.py +11 -0
  30. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/__init__.py +13 -5
  31. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/glob.py +42 -4
  32. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/stat.py +5 -0
  33. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/sync.py +15 -0
  34. pathlib_next-0.6.0/tests/conftest.py +63 -0
  35. pathlib_next-0.6.0/tests/test_archive_uri.py +198 -0
  36. pathlib_next-0.6.0/tests/test_contract.py +29 -0
  37. pathlib_next-0.6.0/tests/test_data_uri.py +76 -0
  38. pathlib_next-0.6.0/tests/test_ftp.py +313 -0
  39. pathlib_next-0.6.0/tests/test_glob.py +91 -0
  40. pathlib_next-0.6.0/tests/test_http.py +70 -0
  41. pathlib_next-0.6.0/tests/test_mempath.py +155 -0
  42. pathlib_next-0.6.0/tests/test_parity_io.py +159 -0
  43. pathlib_next-0.6.0/tests/test_parity_pure.py +102 -0
  44. pathlib_next-0.6.0/tests/test_pathname.py +136 -0
  45. pathlib_next-0.6.0/tests/test_query.py +45 -0
  46. pathlib_next-0.6.0/tests/test_sftp.py +158 -0
  47. pathlib_next-0.6.0/tests/test_smoke.py +95 -0
  48. pathlib_next-0.6.0/tests/test_source.py +62 -0
  49. pathlib_next-0.6.0/tests/test_sync.py +122 -0
  50. pathlib_next-0.4.1/tests/test_uri.py → pathlib_next-0.6.0/tests/test_uri_parse.py +47 -0
  51. pathlib_next-0.6.0/tests/test_uri_path.py +100 -0
  52. pathlib_next-0.6.0/tests/test_utils.py +68 -0
  53. pathlib_next-0.4.1/CHANGELOG.md +0 -39
  54. pathlib_next-0.4.1/PKG-INFO +0 -113
  55. pathlib_next-0.4.1/README.md +0 -79
  56. pathlib_next-0.4.1/docs/index.md +0 -29
  57. pathlib_next-0.4.1/examples/example.py +0 -70
  58. pathlib_next-0.4.1/src/pathlib_next/fspath.py +0 -85
  59. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/LICENSE +0 -0
  60. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/docs/api/reference.md +0 -0
  61. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/docs/changelog.md +0 -0
  62. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/__init__.py +0 -0
  63. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/__init__.py +0 -0
  64. {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/py.typed +0 -0
  65. {pathlib_next-0.4.1 → pathlib_next-0.6.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,243 @@
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.6.0] - 2026-07-11
11
+
12
+ ### Added (Phase 7a new schemes, stdlib-only, no new deps)
13
+ - `data:` scheme (RFC 2397, `pathlib_next.uri.schemes.data.DataUri`):
14
+ read-only, no backend/connection -- the entire file content is embedded
15
+ in the URI (`data:[<mediatype>][;base64],<data>`). `stat().st_size` is
16
+ the decoded payload length; `iterdir()` raises `NotADirectoryError`
17
+ (it's always a single file); write operations raise `NotImplementedError`.
18
+ - `ftp:`/`ftps:` scheme (`pathlib_next.uri.schemes.ftp.FtpPath`): full
19
+ read/write/list access via stdlib `ftplib`, with a thread-keyed LRU
20
+ connection cache mirroring `sftp.py`. Listing/stat prefer MLSD (RFC
21
+ 3659); servers without it fall back to NLST (listing) and SIZE
22
+ (file-only stat). Writes buffer in memory and upload via STOR/APPE on
23
+ `close()`. `chmod()` uses the common but non-standard `SITE CHMOD`
24
+ extension (may not be supported by every server).
25
+ - `zip:`/`tar:` archive paths (`pathlib_next.uri.schemes.archive`):
26
+ `<scheme>:<archive-uri>!/<inner-path>` (Java-style `!/` separator,
27
+ URI form proposed to and confirmed by the user before implementation).
28
+ The archive half is itself any absolute URI with an explicit scheme, so
29
+ archives are readable straight off any other backend (`file:`, `http:`,
30
+ `sftp:`, `ftp:`, `data:`, ...). Read is supported for both schemes.
31
+ Write is `zip:`-only, and only for brand-new entries in a local (`file:`)
32
+ outer archive (overwriting/deleting/renaming an existing entry would
33
+ need a full-archive rewrite -- not implemented, raises
34
+ `NotImplementedError`). `tar:` auto-detects `.tar.gz`/`.tar.bz2`/`.tar.xz`
35
+ compression and is always read-only.
36
+
37
+ ## [0.5.0] - 2026-07-11
38
+
39
+ ### Fixed (critical -- found while writing Phase 6 examples)
40
+ - `Path("...")` -- the top-level dispatcher documented in this project's
41
+ own README quick start and used throughout -- silently dropped its
42
+ constructor arguments on Python <3.12, leaving a blank instance that
43
+ crashed with `AttributeError: _drv` the moment anything touched it (e.g.
44
+ the `/` operator). Masked on 3.12+, where the real parsing happens in
45
+ `__init__` (called separately, with the original args, regardless of what
46
+ `__new__` did) rather than `__new__` itself. Every one of Phase 5's 300
47
+ tests constructed via `LocalPath(...)` directly instead, so this went
48
+ undetected until `examples/local_and_mem.py` exercised the documented
49
+ `Path(...)` entry point end to end.
50
+
51
+ ### Fixed (found by the new Phase 5 test suite, not in the original bug list)
52
+ - `LocalPath.stat()`/`chmod()` inherit directly from `pathlib.Path` via MRO
53
+ and crashed with `TypeError` on Python 3.9 the moment anything passed
54
+ `follow_symlinks=` (e.g. `Path.walk()`'s default `follow_symlinks=False`)
55
+ -- now shimmed with `lstat()`/`lchmod()` on <3.10, same as the existing
56
+ `FileUri` shim (which now just delegates to `LocalPath`).
57
+ - `MemPath.__init__` decided whether to propagate a parent's backend with
58
+ `if _backend and backend is None:` -- an empty (but valid) backend dict is
59
+ falsy, so joining off a freshly-created, empty `MemPath` silently gave the
60
+ child a disconnected new backend instead of sharing the parent's.
61
+ - `MemPath.stat()` never set `st_size` for files (always defaulted to `0`),
62
+ breaking any size-based checksum comparison (notably `PathSyncer`'s
63
+ typical usage).
64
+ - `glob()`'s core algorithm decided whether to recurse into the *parent*
65
+ directory using whether the *leaf* segment is a wildcard, instead of
66
+ whether the *parent path itself* contains one. Since a wildcarded leaf
67
+ with a literal parent directory is the overwhelmingly common case
68
+ (`glob("*.py")`), this always took the "recurse into parent" branch,
69
+ which only degenerated back to the correct single directory when the
70
+ parent has a non-empty literal name to re-match against -- true for
71
+ essentially every real filesystem path except an OS root. It silently
72
+ returned the wrong result on `MemPath`'s virtual root (empty name).
73
+ - `HttpPath.iterdir()` gave every subdirectory entry an empty `.name`:
74
+ directory-listing entries for subdirectories carry a trailing `/`
75
+ (`htmllistparse`'s convention), which wasn't stripped before building the
76
+ child's path, and `Pathname.name` derives from the last path segment --
77
+ empty for a trailing-slash path.
78
+ - `SftpPath.rename()` resolved a plain string target relative to `self`
79
+ (joining it as a child, e.g. `"/a.txt".rename("b.txt")` produced
80
+ `"/a.txt/b.txt"`) instead of `self`'s parent (sibling rename).
81
+
82
+ ### Added (test suite)
83
+ - Full pytest suite (`tests/`): pure-path parity against `pathlib.PurePosixPath`
84
+ (`test_parity_pure.py`), local I/O parity against `pathlib.Path`/`os.walk`
85
+ (`test_parity_io.py`), a reusable filesystem-contract mixin run against
86
+ `LocalPath`/`MemPath`/`FileUri` and exported as `pathlib_next.testing.
87
+ PathContract` for third-party `Path`/`UriPath` implementers
88
+ (`test_contract.py`), glob vs. stdlib ground truth (`test_glob.py`), URI
89
+ parsing/scheme-dispatch/query/source coverage, MemPath- and SFTP-specific
90
+ unit tests (SFTP mocked, no real server), HTTP tests against a real stdlib
91
+ `ThreadingHTTPServer`, and `PathSyncer` coverage. 300 tests, ~85% line
92
+ coverage, green on both Python 3.9 and 3.13.
93
+
94
+ ### Added (docs)
95
+ - `docs/guides/schemes.md` (capability matrix per scheme) and
96
+ `docs/guides/extending.md` (both extension tracks, with worked examples
97
+ and `pathlib_next.testing.PathContract` usage). Rewrote `docs/index.md`
98
+ and the README with a 30-second example per scheme and a capability
99
+ matrix. Class-level docstrings added across the package for the rendered
100
+ API reference.
101
+
102
+ ### Changed
103
+ - `examples/example.py` (an unstructured scratch script) split into three
104
+ focused, runnable examples: `examples/local_and_mem.py` (self-contained,
105
+ no network), `examples/http_listing.py` and `examples/sftp_sync.py`
106
+ (network-touching, guarded under `if __name__ == "__main__"`,
107
+ configurable via env vars, fail soft when unreachable/unconfigured).
108
+
109
+ ### Added
110
+ - `Pathname.joinpath()`, `Pathname.full_match()` (3.13 parity, supports `**`
111
+ matching any number of segments), `Pathname.anchor`/`drive`/`root`
112
+ (generic derivation for non-local paths), `Path.rglob()`,
113
+ `read_text(..., newline=)` (3.13 parity), `Path.samefile()` (default
114
+ `st_dev`/`st_ino` comparison when the backend's `stat()` provides them,
115
+ `NotImplementedError` otherwise).
116
+ - `Path.glob()`/`LocalPath.glob()`: `recursive=` now auto-detects (`True` if
117
+ the pattern has a `"**"` component) instead of defaulting to `False`;
118
+ explicit `recursive=True`/`False` still overrides.
119
+ - `Path.copy()`: raises `IsADirectoryError` when the target is an existing
120
+ directory (previously misbehaved); gained `follow_symlinks=`/
121
+ `preserve_metadata=` kwargs, named to match CPython 3.14's `Path.copy()`.
122
+ - `docs/divergences.md`: registry of every deliberate behavioral divergence
123
+ from `pathlib`, with rationale. Linked from the docs nav.
124
+
125
+ ### Fixed
126
+ - `Path.mkdir(parents=True)` created intermediate parents with `exist_ok=False`
127
+ (racy, and wrong when a parent already existed) and dropped the caller's
128
+ `exist_ok` on the final retry.
129
+ - `Path.touch(exist_ok=False)` silently truncated an existing file instead of
130
+ raising `FileExistsError` (pathlib parity).
131
+ - `LocalPath.glob()`'s `dironly` parameter defaulted to `False`, which made the
132
+ `is None` check for trailing-slash directory-only detection dead code.
133
+ - `Stat._st_mode()` only caught `FileNotFoundError`, letting `PermissionError`
134
+ and other `OSError`s propagate out of `exists()`/`is_dir()`/etc. where pathlib
135
+ returns `False`. Also fixed: `follow_symlinks` was accepted but never
136
+ forwarded to the underlying `stat()` call, so `is_symlink()` never actually
137
+ inspected the symlink itself.
138
+ - `MemPath._open()` treated any mode other than `"w"` as a read, so `"a"`/`"x"`
139
+ silently misbehaved; now dispatches `r`/`w`/`x`/`a` correctly and raises
140
+ `NotImplementedError` for anything else. `MemBytesIO.close()` used
141
+ `seek(0);read()` instead of `getvalue()`, losing content if the caller's
142
+ cursor wasn't already at position 0 when closing.
143
+ - `MemPath.normalized` mangled `".."`-escaping paths (e.g. `".."`) into `"."`;
144
+ now normalizes against a virtual root so they clamp at the root instead.
145
+ - `PathAndStat.__getattr__()` returned `None` for any unrecognized attribute
146
+ instead of raising `AttributeError`, breaking `hasattr()`-based logic.
147
+ - `parsedate(None)` / an unparseable date string returned "now" instead of
148
+ epoch 0, which could poison `PathSyncer`'s checksum/freshness comparisons for
149
+ HTTP sources with no `Last-Modified` header.
150
+ - `HttpPath.stat()` used a bare `except:`; cached `_isdir` from a response that
151
+ hadn't been confirmed successful yet (including 404s); and didn't fall back
152
+ to GET when a server rejected `HEAD` with 405.
153
+ - `uri.Query` no longer depends on `uritools`' private `_querydict`/`_querylist`
154
+ helpers (reimplemented locally against the public `uriencode()`).
155
+ - `Uri` join (`_load_parts`): `query`/`fragment` are now resolved with the same
156
+ "last segment that actually sets one wins" rule already used for `source`
157
+ (previously any segment, even one with no query/fragment, would blank out an
158
+ earlier segment's). Join semantics are now documented explicitly:
159
+ pathlib-`joinpath`-like, not RFC 3986 reference resolution, `..` is never
160
+ resolved during join.
161
+ - `Source.is_local()` (DNS lookup) and `get_machine_ips()` are now
162
+ `functools.lru_cache`d -- previously ran on every call.
163
+
164
+ ### Fixed (crash-level bugs)
165
+ - `MemPath.stat()`/`MemPath._open()` returned a `FileNotFoundError` instance instead
166
+ of raising it for a missing path, causing an unrelated `AttributeError` downstream.
167
+ - `LRU.invalidate()` called `self.lock()` instead of using `self.lock` as a context
168
+ manager (`RLock` isn't callable) -- broke the SFTP client reconnect path.
169
+ - `Pathname.match()` had reversed `isinstance()` arguments and compared against
170
+ `str(self)` (which includes scheme/host for `Uri`) instead of `as_posix()`.
171
+ - Glob wildcard detection (`WILCARD_PATTERN`, renamed `WILDCARD_PATTERN`, old name
172
+ kept as an alias) used `.match()` (anchored) instead of `.search()`, so patterns
173
+ like `"foo*"` weren't recognized as wildcards.
174
+ - `Uri` was unhashable (defined `__eq__` without `__hash__`); `__eq__` now also
175
+ returns `NotImplemented` for non-`Pathname`/`str` operands instead of raising.
176
+ - `Uri.is_relative_to()` used `str.startswith()` on normalized path strings, so
177
+ `/foo/bar2` was incorrectly reported as relative to `/foo/bar`; now compares
178
+ path segments.
179
+ - `Uri.relative_to(walk_up=True)` was dead code -- an early guard raised
180
+ `ValueError` before the walk-up loop ever ran.
181
+ - `HttpPath.is_dir()`/`is_file()` tested truthiness of bound methods
182
+ (`self._is_dir`, `self.is_dir`) instead of calling/checking the right attribute,
183
+ so both always returned truthy nonsense.
184
+ - `SftpPath.chmod()` didn't accept `follow_symlinks=`, so the inherited `lchmod()`
185
+ crashed with `TypeError`; now raises `NotImplementedError` for
186
+ `follow_symlinks=False` (paramiko has no `lchmod`).
187
+ - `SftpPath` defined `_rename()`, which nothing ever called -- renamed to
188
+ `rename()` so `move()`/`rename()` actually use SFTP's native rename instead of
189
+ silently falling back to copy+unlink for every move.
190
+ - `Uri.__init__()` used a bare `except:` around `Path.as_uri()` (now
191
+ `except ValueError:`, matching what `as_uri()` actually raises for relative
192
+ paths) and crashed with `AttributeError` when constructing from an
193
+ `os.PathLike` that only implements `__fspath__` (no `as_posix()`).
194
+ - `Path.rm(ignore_error=callable)` never actually called the callable -- both
195
+ branches of its error handler returned the callable object itself.
196
+
197
+ ### Fixed (Python 3.9/3.10 compatibility)
198
+ - Actual Python 3.9/3.10 runtime compatibility (CI previously only tested 3.11/3.13
199
+ and missed these): `LocalPath`/`Uri` case-sensitivity and path-separator detection
200
+ crashed on 3.9-3.11 (`_flavour` object has no `normcase`); `open(mode="r")` crashed
201
+ on <3.10 (`io.text_encoding` is 3.10+); glob pattern compilation crashed on <3.11
202
+ (`re.NOFLAG` is 3.11+); `FileUri.stat()`/`chmod()` crashed on 3.9
203
+ (`pathlib.Path.stat/chmod` gained `follow_symlinks=` in 3.10; raises
204
+ `NotImplementedError` there for `follow_symlinks=False`).
205
+ - `LocalPath._path_separators` returned the env-var list separator (`;`/`:`) instead
206
+ of the path separator, and could include a `None` altsep on POSIX.
207
+
208
+ ### Added
209
+ - `tests/test_smoke.py`: regression coverage for README/example snippets across
210
+ supported Python versions.
211
+
212
+ ## [0.4.1] - 2026-07-11
213
+
214
+ ### Fixed
215
+ - Removed explicit `[tool.hatch.build.targets.wheel]` packages config that caused hatchling to fail resolving `README.md` during editable installs on CI.
216
+ - Converted `README.md` from a symlink (mode `120000`) to a regular file, fixing `git checkout` failures on macOS and Windows runners.
217
+ - Removed agent-tooling references (`AGENTS.md`, `PYTHON.md`) from committed files.
218
+
219
+ ## [0.4.0] - 2026-07-11
220
+
221
+ ### Added
222
+ - Standardized repository layout and relocated examples to `examples/` directory.
223
+ - Configured MkDocs documentation site with dynamic API reference using `mkdocstrings`.
224
+ - Added GitHub Actions workflows for matrix testing (`test.yml`) and release pipelines (`release.yml`).
225
+ - Added typing marker `py.typed` for PEP 561 compliance.
226
+
227
+ ### Changed
228
+ - 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`.
229
+ - Updated package requirement to `requires-python = ">=3.9"`.
230
+
231
+ ## [0.3.5] - 2026-07-11
232
+
233
+ ### Added
234
+ - Split path into protocols that can be standalone.
235
+ - Sync error handling.
236
+ - Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
237
+
238
+ [Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.6.0...HEAD
239
+ [0.6.0]: https://github.com/jose-pr/pathlib_next/compare/v0.5.0...v0.6.0
240
+ [0.5.0]: https://github.com/jose-pr/pathlib_next/compare/v0.4.1...v0.5.0
241
+ [0.4.1]: https://github.com/jose-pr/pathlib_next/compare/v0.4.0...v0.4.1
242
+ [0.4.0]: https://github.com/jose-pr/pathlib_next/releases/tag/v0.4.0
243
+ [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.6.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).