Skip to content
jose-prPublic

About

Path Protocol and uri implementation based on pathlib

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

pathapi

Version Python versions License: MIT Docs CI

A robust, extensible pathlib-like base for any resource addressable as a path. Same method names, signatures, semantics and exception types as pathlib.Path wherever a pathlib.Path equivalent exists: write code once against Path and it runs against your local disk, an in-memory tree, an FTP server or the members of a zip or tar file, and against any path class built on it. It has no dependencies. Deliberate differences from pathlib are listed in Divergences from pathlib.

pathapi was published as pathlib-next up to 0.10.0; pip install pathlib-next now installs a shim that depends on this package. See the changelog for the migration from pathlib_next.

Features

Class Read Write List Stat mkdir Delete rename
local path (LocalPath) Yes Yes Yes Yes Yes Yes Yes
in-memory (MemPath) Yes Yes Yes Yes Yes Yes No (move() copies)
FTP and FTPS (FTPPath) Yes Yes Yes (MLSD, NLST fallback) Yes Yes Yes Yes
zip members (ZipPath) Yes Local archive only Yes Yes Local archive only Local archive only Local archive only
tar members (TarPath, incl. gz/bz2/xz) Yes No Yes Yes No No No
ArchivePath As ZipPath/TarPath for the format it detects

Every class gets glob(), walk(), copy()/move(), rm() and PathSyncer from the base classes; LocalPath and the other classes replace some of them where the backend has a faster way.

  • One path interface over local files, in-memory trees, FTP servers and archive members, and over any class built on Path.
  • MemPath: a virtual filesystem for tests, mocks and transient data.
  • FTPPath, ZipPath, TarPath, ArchivePath: an FTP server and the members of a zip or tar file as paths, on the standard library alone.
  • pathapi.http: HTTPClient and a transport contract for HTTP requests on the standard library (redirects that never carry Authorization to another origin, TLS verified by default, a proxies= provider); another HTTP library plugs in as a transport through the pathapi.http_transports entry-point group. AsyncHTTPClient is the same client for an event loop, over async transports registered in pathapi.http_async_transports.
  • PathSyncer: one-way, content-compared tree sync between any two implementations, with dry runs and event hooks.
  • Extensible: subclass Path for any resource, and verify it with the bundled pytest contracts.

Paths for other places are separate distributions built on this one:

Distribution Provides
uripath URIPath: one class for any URI, with the schemes file:, data:, ftp:, zip:, tar:, http: and dav:, and the uripath command

Installation

pip install pathapi

There are no extras and no dependencies: everything above runs on the standard library. pytest is needed only to import pathapi.testing and pathapi.http.testing.

Quick start

Local filesystem, as pathlib.Path:

from pathapi import Path

p = Path("data") / "report.txt"
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("hello")
print(p.read_text())

In memory, no disk I/O:

from pathapi.mempath import MemPath

p = MemPath("/config/settings.json")
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text('{"debug": true}')
print([child.name for child in p.parent.iterdir()])

Archive members: the segments are the member path and the archive is any path:

import zipfile
from pathapi import ZipPath

with zipfile.ZipFile("backup.zip", "w") as archive:
    archive.writestr("etc/config.ini", "[main]\n")

member = ZipPath("etc/config.ini", archive="backup.zip")
print(member.read_text())
print([child.name for child in member.parent.iterdir()])

FTP: nothing connects until the first operation:

from pathapi import FTPPath

for child in FTPPath("/pub", host="ftp.example.com", user="anonymous").iterdir():
    print(child.name, child.stat().st_size)

Copy and sync across classes:

from pathapi import Path
from pathapi.mempath import MemPath
from pathapi.sync import PathSyncer

source = MemPath("/site")
(source / "css").mkdir(parents=True)
(source / "index.html").write_text("<h1>hi</h1>")

PathSyncer(remove_missing=True).sync(source, Path("site-copy"))
print(sorted(p.name for p in Path("site-copy").iterdir()))

API overview

Module Purpose
pathapi.path Pathname/Path base classes and the derived operations
pathapi.fspath LocalPath, PosixPathname, WindowsPathname
pathapi.mempath MemPath in-memory filesystem
pathapi.ftp FTPPath, FTPBackend (FTP and FTPS on ftplib)
pathapi.archive ZipPath, TarPath, ArchivePath (members of a zip or tar file), make_archive, unpack_archive
pathapi.http HTTPClient, HTTPTransport, HTTPResponse, AsyncHTTPClient, ThreadedTransport (HTTP requests on http.client, and from an event loop)
pathapi.protocols Stat, Chmod, BinaryOpen, NativeChecksum
pathapi.sync PathSyncer, SyncEvent, PathAndStat
pathapi.glob the glob engine behind Path.glob()
pathapi.checksum md5, sha256, stream, native
pathapi.providers the helpers a class built on Path calls
pathapi.exceptions PathError, NonRelativePatternError
pathapi.testing pytest contracts for custom implementations
pathapi.http.testing testing support (not stable API): the pytest suite that holds an HTTP transport to the promises of the built-in one

The full reference is on the documentation site; an agent-oriented API summary ships inside the package as pathapi/AGENTS.md.

Async

AsyncPath(path) wraps any pathapi path for use inside an event loop: the pure members stay plain, every method that does I/O is a coroutine of the same name, listings are async iterators and open() gives an AsyncFile. Each call runs the sync method on a worker thread, so every backend works at once and the loop is never blocked; it is not a native async client.

import asyncio
from pathapi import AsyncPath, LocalPath

async def main():
    root = AsyncPath(LocalPath("."))
    async for entry in root.iterdir():
        print(entry.name, await entry.is_dir())

asyncio.run(main())

Extending

Subclass Path for a resource of your own (MemPath is the reference), as worked through in Extending. pathapi.testing provides the pytest contracts (PurePathContract, ReadPathContract, PathContract, AsyncPathContract) and populate_fixture_tree() that every class of the package and of the distributions above is verified with.

Supported Python versions

Python 3.9 through 3.14. CI runs the full suite on Ubuntu for every version from 3.9 to 3.14, on Windows and macOS for 3.9 and 3.14, and with nothing but pytest and pyftpdlib installed on 3.9 and 3.14.

Development

pip install -e ".[dev]"
python -m pytest -q -rs

Tests that need an optional package are skipped when it is missing, so check the skip list (-rs). Contributor notes (virtual environments, formatting, CI, releases) are in the repository's AGENTS.md; benchmarks are described in Benchmarks. To preview the documentation: pip install -e ".[docs]" then mkdocs serve.

Changes are recorded in the changelog.

Releasing

  1. Move the [Unreleased] entries of CHANGELOG.md under a new ## [x.y.z] - <date> heading and add its link definition at the bottom.
  2. Bump version in pyproject.toml in the same commit.
  3. Before 1.0 the minor version changes only when the documented API breaks; new methods, new optional arguments and fixes are patch releases.
  4. Run the full suite on the oldest and the newest supported Python, mkdocs build --strict and python -m build.
  5. Push main, then the v* tag: the release workflow tests, builds and publishes to PyPI and creates the GitHub release.

License

MIT; see LICENSE.

About

Path Protocol and uri implementation based on pathlib

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages