Skip to content

Testing Contracts API

pathlib_next.testing

Test-suite building blocks for verifying custom Path/UriPath implementations satisfy the library's filesystem contract.

Not imported by pathlib_next/__init__.py -- this module requires pytest, which is a test-only dependency. Import it explicitly. Example::

import pytest

from pathlib_next import LocalPath as MyPath  # your Path subclass here
from pathlib_next.testing import PathContract, populate_fixture_tree


class TestMyPath(PathContract):
    @pytest.fixture
    def root(self, tmp_path):
        root = MyPath(tmp_path)
        populate_fixture_tree(root)
        return root

root must be a fresh, function-scoped directory holding exactly the standard tree (populate_fixture_tree() builds it through the path's own API): the write tests create fixed names and assert their absence first, so two contract classes sharing one root fail each other.

Rules pathlib guarantees are asserted with pathlib's exception types. Where pathlib itself differs by OS (reading or unlinking a directory raises IsADirectoryError on Linux and PermissionError on Windows/macOS), the contract accepts that documented set. A backend that genuinely cannot meet a rule sets the matching capability attribute to False on its test class; the affected tests then report as skipped, never as passed:

  • ReadPathContract.supports_listing -- iterdir(), glob(), walk().
  • ReadPathContract.supports_empty_directories -- empty_dir/ lists as empty (git trees cannot hold an empty directory, so a placeholder file lives there).
  • ReadPathContract.distinguishes_file_types -- listing a file raises NotADirectoryError and reading a directory raises (plain HTTP cannot tell: one URL serves an index page or a file).
  • PathContract.supports_rename -- rename().
  • PathContract.supports_append -- open("a")/open("ab").
  • PathContract.supports_exclusive_create -- open("x").
  • PathContract.enforces_directory_hierarchy -- mkdir() and writes below a missing parent raise FileNotFoundError, and writing a file over a directory raises (object stores, whose directories are key prefixes, cannot).

PathContract

Bases: ReadPathContract

Mixin of filesystem-contract tests every writable Path implementation (custom Path subclass, or UriPath scheme) must satisfy.

Subclasses must provide a root fixture pointing to a fresh, writable, function-scoped directory pre-populated with the standard tree (see populate_fixture_tree()). Tests create fixed names under it without cleaning up.

Capability attributes (all default True; set one False only for a documented gap): supports_rename, supports_append, supports_exclusive_create, enforces_directory_hierarchy.

PurePathContract

Contract tests for pure path (logical) operations, not requiring any I/O.

ReadPathContract

Bases: PurePathContract

Contract tests for read-only path operations.

Subclasses must provide a root fixture pointing to a fresh directory pre-populated with the standard tree -- see populate_fixture_tree() and FIXTURE_TREE:

  • a.txt (content: "a")
  • b.py (content: "b")
  • .hidden.txt (content: "hidden")
  • sub/c.py (content: "c")
  • sub/nested/d.py (content: "d")
  • empty_dir/

Capability attributes (all default True; set one False only for a documented gap, and the affected tests skip): supports_listing, supports_empty_directories, distinguishes_file_types.

populate_fixture_tree(root)

Create the standard contract tree (FIXTURE_TREE) under root, an existing empty directory, through the path's own mkdir() and write_text() -- works for any Path implementation (or a stdlib pathlib.Path). Returns root.

Source code in src/pathlib_next/testing.py
75
76
77
78
79
80
81
82
83
84
85
86
def populate_fixture_tree(root):
    """Create the standard contract tree (`FIXTURE_TREE`) under `root`, an
    existing empty directory, through the path's own `mkdir()` and
    `write_text()` -- works for any `Path` implementation (or a stdlib
    `pathlib.Path`). Returns `root`."""
    for name, content in FIXTURE_TREE.items():
        path = root.joinpath(*name.split("/"))
        if content is None:
            path.mkdir()
        else:
            path.write_text(content)
    return root