Skip to content

Testing Contracts API

pathapi.testing

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

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

import pytest

from pathapi import LocalPath as MyPath  # your Path subclass here
from pathapi.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. Every attribute defaults to True except supports_question_mark_names and the five that default to None, meaning "ask supports()" (supports_listing, supports_mkdir, supports_delete, supports_utime, supports_rename):

  • ReadPathContract.supports_listing -- iterdir(), glob(), walk() (a listing's stat whose size_known is False is held to its type only).
  • 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).
  • ReadPathContract.supports_pickle -- a path pickles, and the copy is equal to it and hashes alike.
  • 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, writing a file over a directory raises, and rename() onto a non-empty directory raises (object stores, whose directories are key prefixes, cannot).
  • PathContract.supports_mkdir -- mkdir(), and so every test that creates a directory (a transport with no directories, such as TFTP, has none).
  • PathContract.supports_delete -- unlink(), rmdir() and rm(), and replacing a file in copy(overwrite=True).
  • PathContract.supports_move -- move(), by rename() or by copying and then deleting; False for a backend that can do neither.
  • PathContract.supports_unusual_names -- names holding a space, #, % (also followed by two hex digits), +, &, = and mixed case are stored and listed exactly as written.
  • PathContract.supports_question_mark_names -- the same for names holding ?. False by default: a Windows file system cannot hold such a name, so a correct local backend there would fail; set it True for a URI scheme or a remote store.
  • PathContract.supports_utime -- _utime() sets an existing file's modification time, and touch() of that file moves it. A backend that keeps no settable time (an object store, an archive) is correct without it: it leaves _utime() to raise NotImplementedError and sets this False.

A test that needs an operation checks the matching switch first, so a backend without that operation needs no expected-failure marks. The tree itself is yours to build: populate_fixture_tree() uses mkdir() and write_text(), so a backend without them seeds root another way.

DIRECTORY_ERRORS = (IsADirectoryError, PermissionError) module-attribute

FIXTURE_TREE = {'a.txt': 'a', 'b.py': 'b', '.hidden.txt': 'hidden', 'sub': None, 'sub/c.py': 'c', 'sub/nested': None, 'sub/nested/d.py': 'd', 'empty_dir': None} module-attribute

NOT_EMPTY_ERRNOS = (_errno.ENOTEMPTY, _errno.EEXIST) module-attribute

__all__ = ['DIRECTORY_ERRORS', 'FIXTURE_TREE', 'NOT_EMPTY_ERRNOS', 'PathContract', 'PurePathContract', 'ReadPathContract', 'populate_fixture_tree'] module-attribute

PathContract

Bases: ReadPathContract

Mixin of filesystem-contract tests every writable Path implementation (a custom Path subclass) 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 except supports_question_mark_names; set one False only for a documented gap): supports_rename, supports_append, supports_exclusive_create, enforces_directory_hierarchy, supports_mkdir, supports_delete, supports_move, supports_unusual_names, supports_utime, and supports_question_mark_names (default False).

enforces_directory_hierarchy = True class-attribute instance-attribute

supports_append = True class-attribute instance-attribute

supports_delete = None class-attribute instance-attribute

supports_exclusive_create = True class-attribute instance-attribute

supports_mkdir = None class-attribute instance-attribute

supports_move = True class-attribute instance-attribute

supports_question_mark_names = False class-attribute instance-attribute

supports_rename = None class-attribute instance-attribute

supports_unusual_names = True class-attribute instance-attribute

supports_utime = None class-attribute instance-attribute

test_copy_existing_target_raises_without_overwrite(root)

Source code in src/pathapi/testing/_contract.py
356
357
358
359
360
361
362
363
364
365
def test_copy_existing_target_raises_without_overwrite(self, root):
    src = root / "a.txt"
    dst = root / "dst_copy_existing.txt"
    dst.write_text("existing")
    with pytest.raises(FileExistsError):
        src.copy(dst)
    assert dst.read_text() == "existing"
    if self._has("supports_delete", root):  # replacing a file removes it first
        src.copy(dst, overwrite=True)
        assert dst.read_text() == "a"

test_copy_missing_source_raises_file_not_found(root)

Source code in src/pathapi/testing/_contract.py
367
368
369
370
371
def test_copy_missing_source_raises_file_not_found(self, root):
    dst = root / "dst_copy_missing.txt"
    with pytest.raises(FileNotFoundError):
        (root / "nonexistent.txt").copy(dst)
    assert not dst.exists()

test_copy_onto_itself_raises_and_keeps_content(root)

Source code in src/pathapi/testing/_contract.py
373
374
375
376
377
378
379
def test_copy_onto_itself_raises_and_keeps_content(self, root):
    # A second spelling of the same file, built separately: truncating
    # the "target" before reading the source would destroy the only copy.
    src = root / "a.txt"
    with pytest.raises(OSError):
        src.copy(root / "a.txt", overwrite=True)
    assert src.read_text() == "a"

test_copy_preserves_source(root)

Source code in src/pathapi/testing/_contract.py
349
350
351
352
353
354
def test_copy_preserves_source(self, root):
    src = root / "a.txt"
    dst = root / "dst_copy.txt"
    src.copy(dst)
    assert dst.read_text() == "a"
    assert src.exists()

test_copy_recursive(root)

Source code in src/pathapi/testing/_contract.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def test_copy_recursive(self, root):
    self._require("supports_mkdir", root)
    dst = root / "sub_copy"
    (root / "sub").copy(dst, recursive=True)
    assert (dst / "c.py").read_text() == "c"
    assert (dst / "nested" / "d.py").read_text() == "d"
    assert (root / "sub" / "nested" / "d.py").exists()

    (root / "sub" / "c.py").write_text("changed")
    with pytest.raises(FileExistsError):
        (root / "sub").copy(dst, recursive=True)
    assert (dst / "c.py").read_text() == "c"
    if self._has("supports_delete", root):  # replacing a file removes it first
        (root / "sub").copy(dst, recursive=True, overwrite=True)
        assert (dst / "c.py").read_text() == "changed"
        assert (dst / "nested" / "d.py").read_text() == "d"

test_every_byte_value_survives_a_multi_chunk_round_trip(root)

Source code in src/pathapi/testing/_contract.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def test_every_byte_value_survives_a_multi_chunk_round_trip(self, root):
    # Larger than any one transfer block, with every byte value and runs
    # of CR/LF: a transport that translates line ends, keeps seven bits,
    # or stops at its first chunk changes the bytes or the length.
    payload = _payload()
    f = root / "payload.bin"
    f.write_bytes(payload)
    assert f.stat().st_size == len(payload)
    assert f.read_bytes() == payload

    g = root / "payload_streamed.bin"
    with g.open("wb") as fh:
        fh.write(payload[:7])
        fh.write(payload[7:70000])
        fh.write(payload[70000:])
    assert g.read_bytes() == payload

    with f.open("rb") as fh:
        assert fh.read(100) == payload[:100]
        assert fh.read(70000) == payload[100:70100]
        assert fh.read() == payload[70100:]

test_listing_reflects_writes(root)

Source code in src/pathapi/testing/_contract.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def test_listing_reflects_writes(self, root):
    self._require("supports_listing")
    (root / "empty_dir" / "new.txt").write_text("n")
    if self._has("supports_mkdir", root):
        (root / "empty_dir" / "new_sub").mkdir()
    expected = (
        {"new.txt", "new_sub"} if self._has("supports_mkdir", root) else {"new.txt"}
    )
    assert {p.name for p in (root / "empty_dir").iterdir()} == expected
    if not self._has("supports_delete", root):
        return
    (root / "empty_dir" / "new.txt").unlink()
    if self._has("supports_mkdir", root):
        (root / "empty_dir" / "new_sub").rmdir()
    assert list((root / "empty_dir").iterdir()) == []

test_mkdir_and_is_dir(root)

Source code in src/pathapi/testing/_contract.py
45
46
47
48
49
50
51
def test_mkdir_and_is_dir(self, root):
    self._require("supports_mkdir", root)
    d = root / "new_dir"
    assert not d.exists()
    d.mkdir()
    assert d.is_dir()
    assert not d.is_file()

test_mkdir_existing_raises_file_exists(root)

Source code in src/pathapi/testing/_contract.py
53
54
55
56
57
58
59
60
61
def test_mkdir_existing_raises_file_exists(self, root):
    self._require("supports_mkdir", root)
    with pytest.raises(FileExistsError):
        (root / "sub").mkdir()
    (root / "sub").mkdir(exist_ok=True)
    # exist_ok never covers a file in the way.
    with pytest.raises(FileExistsError):
        (root / "a.txt").mkdir(exist_ok=True)
    assert (root / "a.txt").read_text() == "a"

test_mkdir_missing_parent_raises_file_not_found(root)

Source code in src/pathapi/testing/_contract.py
63
64
65
66
67
68
def test_mkdir_missing_parent_raises_file_not_found(self, root):
    self._require("supports_mkdir", root)
    self._require("enforces_directory_hierarchy")
    with pytest.raises(FileNotFoundError):
        (root / "missing_parent" / "child").mkdir()
    assert not (root / "missing_parent").exists()

test_mkdir_parents(root)

Source code in src/pathapi/testing/_contract.py
499
500
501
502
503
504
505
506
def test_mkdir_parents(self, root):
    self._require("supports_mkdir", root)
    d = root / "parent_a" / "parent_b" / "parent_c"
    d.mkdir(parents=True)
    assert d.is_dir()
    with pytest.raises(FileExistsError):
        d.mkdir(parents=True, exist_ok=False)
    d.mkdir(parents=True, exist_ok=True)

test_move(root)

Source code in src/pathapi/testing/_contract.py
408
409
410
411
412
413
414
415
416
def test_move(self, root):
    self._require("supports_move")
    # We write a temp file to move so we don't destroy a.txt for other tests
    src = root / "src_move.txt"
    src.write_text("data")
    dst = root / "dst_move.txt"
    src.move(dst)
    assert not src.exists()
    assert dst.read_text() == "data"

test_move_directory(root)

Source code in src/pathapi/testing/_contract.py
431
432
433
434
435
436
437
438
439
def test_move_directory(self, root):
    self._require("supports_move")
    self._require("supports_mkdir", root)
    src = root / "sub"
    dst = root / "moved_sub"
    src.move(dst)
    assert not src.exists()
    assert (dst / "c.py").read_text() == "c"
    assert (dst / "nested" / "d.py").read_text() == "d"

test_move_existing_target_raises_without_overwrite(root)

Source code in src/pathapi/testing/_contract.py
418
419
420
421
422
423
424
425
426
427
428
429
def test_move_existing_target_raises_without_overwrite(self, root):
    self._require("supports_move")
    src = root / "src_move_existing.txt"
    src.write_text("data")
    dst = root / "b.py"
    with pytest.raises(FileExistsError):
        src.move(dst)
    assert src.read_text() == "data"
    assert dst.read_text() == "b"
    src.move(dst, overwrite=True)
    assert not src.exists()
    assert dst.read_text() == "data"

test_names_with_a_question_mark_are_stored_and_listed_as_written(root)

Source code in src/pathapi/testing/_contract.py
109
110
111
def test_names_with_a_question_mark_are_stored_and_listed_as_written(self, root):
    self._require("supports_question_mark_names")
    self._round_trip_names(root, QUESTION_MARK_NAMES)

test_names_with_url_characters_are_stored_and_listed_as_written(root)

Source code in src/pathapi/testing/_contract.py
105
106
107
def test_names_with_url_characters_are_stored_and_listed_as_written(self, root):
    self._require("supports_unusual_names")
    self._round_trip_names(root, UNUSUAL_NAMES)

test_one_error_predicate_means_the_same_to_rm_and_copy(root)

Source code in src/pathapi/testing/_contract.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def test_one_error_predicate_means_the_same_to_rm_and_copy(self, root):
    # `(error, path) -> bool`: True skips the entry, False raises, and a predicate that
    # raises ends the call with its own exception. `rm()` is offered a missing path,
    # `copy()` a child whose target is a directory.
    self._require("supports_mkdir", root)

    class Stop(Exception):
        pass

    def attempt(call):
        try:
            call()
        except Stop:
            return "stop"
        except OSError:
            return "error"
        return "returned"

    outcomes = {}
    for decision in ("swallow", "decline", "raise"):
        offered = []

        def predicate(error, path):
            offered.append((isinstance(error, OSError), path.name))
            if decision == "raise":
                raise Stop()
            return decision == "swallow"

        source = root / f"predicate_{decision}_src"
        target = root / f"predicate_{decision}_dst"
        source.mkdir()
        (source / "clash.txt").write_text("source")
        (target / "clash.txt").mkdir(parents=True)
        (target / "clash.txt" / "kept.txt").write_text("target")

        removed = attempt(
            lambda: (root / f"predicate_{decision}_gone").rm(ignore_error=predicate)
        )
        copied = attempt(
            lambda: source.copy(
                target, recursive=True, overwrite=True, on_error=predicate
            )
        )
        outcomes[decision] = (removed, copied)
        assert offered == [
            (True, f"predicate_{decision}_gone"),
            (True, "clash.txt"),
        ], decision
        assert (target / "clash.txt" / "kept.txt").read_text() == "target"

    assert outcomes == {
        "swallow": ("returned", "returned"),
        "decline": ("error", "error"),
        "raise": ("stop", "stop"),
    }

test_open_append(root)

Source code in src/pathapi/testing/_contract.py
166
167
168
169
170
171
172
173
174
175
176
def test_open_append(self, root):
    self._require("supports_append")
    f = root / "append.txt"
    with f.open("a") as fh:  # creates a missing file
        fh.write("a")
    assert f.read_text() == "a"
    with f.open("a") as fh:
        fh.write("b")
    with f.open("ab") as fh:
        fh.write(b"c")
    assert f.read_text() == "abc"

test_open_exclusive_create(root)

Source code in src/pathapi/testing/_contract.py
178
179
180
181
182
183
184
185
186
187
188
def test_open_exclusive_create(self, root):
    self._require("supports_exclusive_create")
    f = root / "exclusive.txt"
    with f.open("x") as fh:
        fh.write("new")
    assert f.read_text() == "new"
    with pytest.raises(FileExistsError):
        f.open("x")
    with pytest.raises(FileExistsError):
        f.open("xb")
    assert f.read_text() == "new"

test_open_write_truncates(root)

Source code in src/pathapi/testing/_contract.py
153
154
155
156
157
158
159
160
161
162
163
164
def test_open_write_truncates(self, root):
    f = root / "write_trunc.txt"
    f.write_text("long content")
    with f.open("w") as fh:
        fh.write("x")
    assert f.read_text() == "x"
    with f.open("wt") as fh:
        fh.write("yz")
    assert f.read_text() == "yz"
    with f.open("wb") as fh:
        fh.write(b"\x00")
    assert f.read_bytes() == b"\x00"

test_rename(root)

Source code in src/pathapi/testing/_contract.py
441
442
443
444
445
446
447
448
449
450
451
def test_rename(self, root):
    self._require("supports_rename", root)
    src = root / "src_rename.txt"
    src.write_text("data")
    dst = root / "dst_rename.txt"
    result = src.rename(dst)
    assert result.name == "dst_rename.txt"
    assert not src.exists()
    assert dst.read_text() == "data"
    with pytest.raises(FileNotFoundError):
        src.rename(root / "never.txt")

test_rename_onto_a_non_empty_directory_raises_and_keeps_both(root)

Source code in src/pathapi/testing/_contract.py
453
454
455
456
457
458
459
460
461
462
463
464
465
def test_rename_onto_a_non_empty_directory_raises_and_keeps_both(self, root):
    self._require("supports_rename", root)
    self._require("enforces_directory_hierarchy")
    src = root / "src_rename_onto_dir.txt"
    src.write_text("data")
    with pytest.raises(OSError):
        src.rename(root / "sub")
    assert src.read_text() == "data"
    assert (root / "sub" / "c.py").read_text() == "c"
    with pytest.raises(OSError):
        (root / "empty_dir").rename(root / "sub")
    assert (root / "empty_dir").is_dir()
    assert (root / "sub" / "nested" / "d.py").read_text() == "d"

test_rm_missing_ok(root)

Source code in src/pathapi/testing/_contract.py
287
288
289
290
291
def test_rm_missing_ok(self, root):
    self._require("supports_delete", root)
    with pytest.raises(FileNotFoundError):
        (root / "missing_rm").rm()
    (root / "missing_rm").rm(missing_ok=True)

test_rm_non_recursive_directory_requires_empty(root)

Source code in src/pathapi/testing/_contract.py
278
279
280
281
282
283
284
285
def test_rm_non_recursive_directory_requires_empty(self, root):
    self._require("supports_delete", root)
    with pytest.raises(OSError) as info:
        (root / "sub").rm()
    assert info.value.errno in NOT_EMPTY_ERRNOS
    assert (root / "sub" / "c.py").exists()
    (root / "empty_dir").rm()
    assert not (root / "empty_dir").exists()

test_rm_recursive(root)

Source code in src/pathapi/testing/_contract.py
236
237
238
239
240
241
242
243
244
245
def test_rm_recursive(self, root):
    self._require("supports_mkdir", root)
    self._require("supports_delete", root)
    d = root / "new_rm_rec"
    d.mkdir()
    (d / "f.txt").write_text("x")
    (d / "sub_rec").mkdir()
    (d / "sub_rec" / "g.txt").write_text("y")
    d.rm(recursive=True)
    assert not d.exists()

test_rm_recursive_accepts_the_follow_policies(root)

Source code in src/pathapi/testing/_contract.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def test_rm_recursive_accepts_the_follow_policies(self, root):
    # `rm()` takes `follow_symlinks=` and `follow_binds=` on every
    # implementation; a tree holding no link or binding comes out the
    # same under each of them.
    self._require("supports_mkdir", root)
    self._require("supports_delete", root)
    policies = (
        {"follow_symlinks": False, "follow_binds": False},
        {"follow_symlinks": True, "follow_binds": True},
        {"follow_symlinks": None, "follow_binds": None},
        {"follow_symlinks": lambda path: False, "follow_binds": lambda path: None},
    )
    for index, keywords in enumerate(policies):
        d = root / f"new_rm_policy_{index}"
        d.mkdir()
        (d / "f.txt").write_text("x")
        (d / "sub_pol").mkdir()
        (d / "sub_pol" / "g.txt").write_text("y")
        d.rm(recursive=True, **keywords)
        assert not d.exists()

test_rm_rejects_a_policy_that_is_not_one(root)

Source code in src/pathapi/testing/_contract.py
268
269
270
271
272
273
274
275
276
def test_rm_rejects_a_policy_that_is_not_one(self, root):
    self._require("supports_mkdir", root)
    self._require("supports_delete", root)
    d = root / "new_rm_bad_policy"
    d.mkdir()
    (d / "f.txt").write_text("x")
    with pytest.raises(ValueError):
        d.rm(recursive=True, ignore_error=True, follow_symlinks="always")
    assert (d / "f.txt").read_text() == "x"

test_rmdir_file_raises_not_a_directory(root)

Source code in src/pathapi/testing/_contract.py
230
231
232
233
234
def test_rmdir_file_raises_not_a_directory(self, root):
    self._require("supports_delete", root)
    with pytest.raises(NotADirectoryError):
        (root / "a.txt").rmdir()
    assert (root / "a.txt").read_text() == "a"

test_rmdir_missing_raises_file_not_found(root)

Source code in src/pathapi/testing/_contract.py
225
226
227
228
def test_rmdir_missing_raises_file_not_found(self, root):
    self._require("supports_delete", root)
    with pytest.raises(FileNotFoundError):
        (root / "missing_rmdir").rmdir()

test_rmdir_requires_empty(root)

Source code in src/pathapi/testing/_contract.py
211
212
213
214
215
216
217
218
219
220
221
222
223
def test_rmdir_requires_empty(self, root):
    self._require("supports_mkdir", root)
    self._require("supports_delete", root)
    d = root / "new_rmdir"
    d.mkdir()
    (d / "f.txt").write_text("x")
    with pytest.raises(OSError) as info:
        d.rmdir()
    assert info.value.errno in NOT_EMPTY_ERRNOS
    assert (d / "f.txt").read_text() == "x"
    (d / "f.txt").unlink()
    d.rmdir()
    assert not d.exists()

test_same_filesystem_holds_within_one_root(root)

Source code in src/pathapi/testing/_contract.py
381
382
383
384
385
386
387
388
389
def test_same_filesystem_holds_within_one_root(self, root):
    # `copy()`, `move()` and `PathSyncer` only compare two paths that
    # `_same_filesystem()` places together; paths derived from one root
    # always are. A type fronting several hosts or stores overrides it.
    child = root / "a.txt"
    if type(child) is type(root):
        assert root._same_filesystem(child)
        assert child._same_filesystem(root)
    assert child._same_filesystem(root / "sub" / "c.py")

test_touch(root)

Source code in src/pathapi/testing/_contract.py
467
468
469
470
471
472
473
def test_touch(self, root):
    f = root / "touch_new.txt"
    f.touch()
    assert f.is_file()
    assert f.read_bytes() == b""
    (root / "a.txt").touch()
    assert (root / "a.txt").read_text() == "a"

test_touch_exist_ok_false_raises_without_truncating(root)

Source code in src/pathapi/testing/_contract.py
492
493
494
495
496
497
def test_touch_exist_ok_false_raises_without_truncating(self, root):
    f = root / "touch_f.txt"
    f.write_text("keep")
    with pytest.raises(FileExistsError):
        f.touch(exist_ok=False)
    assert f.read_text() == "keep"
Source code in src/pathapi/testing/_contract.py
190
191
192
193
194
195
196
def test_unlink(self, root):
    self._require("supports_delete", root)
    f = root / "write_unlink.txt"
    f.write_text("x")
    assert f.exists()
    f.unlink()
    assert not f.exists()
Source code in src/pathapi/testing/_contract.py
205
206
207
208
209
def test_unlink_directory_raises(self, root):
    self._require("supports_delete", root)
    with pytest.raises(DIRECTORY_ERRORS):
        (root / "empty_dir").unlink()
    assert (root / "empty_dir").is_dir()
Source code in src/pathapi/testing/_contract.py
198
199
200
201
202
203
def test_unlink_missing_raises_then_missing_ok(self, root):
    self._require("supports_delete", root)
    f = root / "missing_unlink.txt"
    with pytest.raises(FileNotFoundError):
        f.unlink()
    f.unlink(missing_ok=True)

test_utime_sets_the_time_of_an_existing_file_and_touch_moves_it(root)

Source code in src/pathapi/testing/_contract.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def test_utime_sets_the_time_of_an_existing_file_and_touch_moves_it(self, root):
    self._require("supports_utime", root)
    f = root / "utime_f.txt"
    f.write_text("kept")
    old = 1_000_000_000
    f._utime(mtime=old)
    assert int(f.stat().st_mtime) == old
    f._utime(mtime=old + 600.5)
    assert int(f.stat().st_mtime) == old + 600
    f._utime(atime=old + 5)
    assert int(f.stat().st_mtime) == old + 600
    f.touch()
    assert f.stat().st_mtime > old + 1_000
    assert f.read_text() == "kept"
    with pytest.raises(FileNotFoundError):
        (root / "utime_absent.txt")._utime(mtime=old)

test_write_directory_raises(root)

Source code in src/pathapi/testing/_contract.py
146
147
148
149
150
151
def test_write_directory_raises(self, root):
    self._require("enforces_directory_hierarchy")
    with pytest.raises(DIRECTORY_ERRORS):
        (root / "sub").write_bytes(b"x")
    assert (root / "sub").is_dir()
    assert (root / "sub" / "c.py").read_text() == "c"

test_write_missing_parent_raises_file_not_found(root)

Source code in src/pathapi/testing/_contract.py
140
141
142
143
144
def test_write_missing_parent_raises_file_not_found(self, root):
    self._require("enforces_directory_hierarchy")
    with pytest.raises(FileNotFoundError):
        (root / "missing_parent" / "y.txt").write_text("y")
    assert not (root / "missing_parent").exists()

test_write_read_bytes_roundtrip(root)

Source code in src/pathapi/testing/_contract.py
78
79
80
81
def test_write_read_bytes_roundtrip(self, root):
    f = root / "write_f.bin"
    f.write_bytes(b"\x00\x01hello")
    assert f.read_bytes() == b"\x00\x01hello"

test_write_read_text_roundtrip(root)

Source code in src/pathapi/testing/_contract.py
70
71
72
73
74
75
76
def test_write_read_text_roundtrip(self, root):
    f = root / "write_f.txt"
    f.write_text("hello")
    assert f.exists()
    assert f.is_file()
    assert not f.is_dir()
    assert f.read_text() == "hello"

PurePathContract

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

test_pure_basics(root)

Source code in src/pathapi/testing/_read.py
26
27
28
29
30
31
32
33
34
35
def test_pure_basics(self, root):
    p = root / "dir" / "file.txt"
    assert p.name == "file.txt"
    assert p.suffix == ".txt"
    assert p.stem == "file"

    # parent and parents
    assert p.parent.name == "dir"
    assert len(p.parents) >= 2
    assert p.parents[0].name == "dir"

test_pure_joinpath_truediv(root)

Source code in src/pathapi/testing/_read.py
37
38
39
40
def test_pure_joinpath_truediv(self, root):
    p = root / "a"
    assert (p / "b").name == "b"
    assert p.joinpath("b", "c").name == "c"

test_pure_match(root)

Source code in src/pathapi/testing/_read.py
42
43
44
45
def test_pure_match(self, root):
    p = root / "dir" / "file.txt"
    assert p.match("*.txt")
    assert not p.match("*.py")

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, supports_pickle.

distinguishes_file_types = True class-attribute instance-attribute

supports_empty_directories = True class-attribute instance-attribute

supports_listing = None class-attribute instance-attribute

supports_pickle = True class-attribute instance-attribute

test_a_pickled_path_is_equal_to_the_original(root)

Source code in src/pathapi/testing/_read.py
117
118
119
120
121
122
123
124
125
126
127
128
129
def test_a_pickled_path_is_equal_to_the_original(self, root):
    # A worker process receives a path by pickle. The copy must be the
    # same value: equal, hashing alike, and for a URI path placed on the
    # same filesystem as the original.
    self._require("supports_pickle")
    path = root / "a.txt"
    again = pickle.loads(pickle.dumps(path))
    assert type(again) is type(path)
    assert again == path
    assert hash(again) == hash(path)
    if hasattr(path, "_supplied_backend"):
        assert again._same_filesystem(path)
        assert path._same_filesystem(again)

test_exists_and_types(root)

Source code in src/pathapi/testing/_read.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def test_exists_and_types(self, root):
    assert root.exists()
    assert root.is_dir()

    a = root / "a.txt"
    assert a.exists()
    assert a.is_file()
    assert not a.is_dir()

    sub = root / "sub"
    assert sub.exists()
    assert sub.is_dir()
    assert not sub.is_file()

    assert not (root / "nonexistent").exists()
    assert not (root / "nonexistent").is_file()
    assert not (root / "nonexistent").is_dir()

test_glob(root)

Source code in src/pathapi/testing/_read.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def test_glob(self, root):
    self._require("supports_listing", root)
    assert {_rel(root, p) for p in root.glob("*.py")} == {"b.py"}
    # pathlib matches hidden names with "*".
    assert {_rel(root, p) for p in root.glob("*")} == {
        "a.txt",
        "b.py",
        ".hidden.txt",
        "sub",
        "empty_dir",
    }
    assert {_rel(root, p) for p in root.glob("sub/*.py")} == {"sub/c.py"}
    # A trailing separator selects directories only -- with `native=False`,
    # the one rule for every interpreter. The default follows the running
    # one, and pathlib ignored a trailing separator before 3.11.
    assert {_rel(root, p) for p in root.glob("*/", native=False)} == {
        "sub",
        "empty_dir",
    }

test_glob_missing_or_file_parent_selects_nothing(root)

Source code in src/pathapi/testing/_read.py
253
254
255
256
257
def test_glob_missing_or_file_parent_selects_nothing(self, root):
    self._require("supports_listing", root)
    assert list((root / "nonexistent").glob("*")) == []
    assert list((root / "a.txt").glob("*")) == []
    assert list(root.glob("*.nonexistent")) == []

test_glob_recursive(root)

Source code in src/pathapi/testing/_read.py
247
248
249
250
251
def test_glob_recursive(self, root):
    self._require("supports_listing", root)
    expected = {"b.py", "sub/c.py", "sub/nested/d.py"}
    assert {_rel(root, p) for p in root.glob("**/*.py")} == expected
    assert {_rel(root, p) for p in root.rglob("*.py")} == expected

test_iterdir_file_raises_not_a_directory(root)

Source code in src/pathapi/testing/_read.py
160
161
162
163
164
def test_iterdir_file_raises_not_a_directory(self, root):
    self._require("supports_listing", root)
    self._require("distinguishes_file_types")
    with pytest.raises(NotADirectoryError):
        list((root / "a.txt").iterdir())

test_iterdir_lists_children(root)

Source code in src/pathapi/testing/_read.py
148
149
150
151
152
153
def test_iterdir_lists_children(self, root):
    self._require("supports_listing", root)
    names = {p.name for p in root.iterdir()}
    assert names == {"a.txt", "b.py", ".hidden.txt", "sub", "empty_dir"}
    if self.supports_empty_directories:
        assert list((root / "empty_dir").iterdir()) == []

test_iterdir_missing_raises_file_not_found(root)

Source code in src/pathapi/testing/_read.py
155
156
157
158
def test_iterdir_missing_raises_file_not_found(self, root):
    self._require("supports_listing", root)
    with pytest.raises(FileNotFoundError):
        list((root / "nonexistent").iterdir())

test_listing_metadata_agrees_with_stat(root)

Source code in src/pathapi/testing/_read.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def test_listing_metadata_agrees_with_stat(self, root):
    # `_scandir()` carries the metadata `walk()`, `rm()` and `PathSyncer` trust without
    # a `stat()` per entry; `None` means unknown, never a different answer, and a
    # stat with `size_known` False answers the type alone.
    self._require("supports_listing", root)
    listed = dict(root._scandir())
    assert set(listed) == {"a.txt", "b.py", ".hidden.txt", "sub", "empty_dir"}
    for name, entry in listed.items():
        if entry is None:
            continue
        real = (root / name).stat()
        assert _stat.S_ISDIR(entry.st_mode) == _stat.S_ISDIR(real.st_mode), name
        if not _stat.S_ISDIR(real.st_mode) and getattr(entry, "size_known", True):
            assert entry.st_size == real.st_size, name

test_nothing_exists_below_a_file(root)

Source code in src/pathapi/testing/_read.py
210
211
212
213
214
215
216
217
218
def test_nothing_exists_below_a_file(self, root):
    below = root / "a.txt" / "x"
    assert below.exists() is False
    assert below.is_file() is False
    assert below.is_dir() is False
    with pytest.raises((FileNotFoundError, NotADirectoryError)):
        below.read_bytes()
    with pytest.raises((FileNotFoundError, NotADirectoryError)):
        below.stat()

test_open_read_modes(root)

Source code in src/pathapi/testing/_read.py
137
138
139
140
141
142
143
144
145
146
def test_open_read_modes(self, root):
    f = root / ".hidden.txt"
    with f.open() as fh:
        assert fh.read() == "hidden"
    with f.open("r") as fh:
        assert fh.read() == "hidden"
    with f.open("rt") as fh:
        assert fh.read() == "hidden"
    with f.open("rb") as fh:
        assert fh.read() == b"hidden"

test_partial_reads_continue_where_the_last_stopped(root)

Source code in src/pathapi/testing/_read.py
220
221
222
223
224
225
def test_partial_reads_continue_where_the_last_stopped(self, root):
    with (root / ".hidden.txt").open("rb") as fh:
        assert fh.read(2) == b"hi"
        assert fh.read(3) == b"dde"
        assert fh.read() == b"n"
        assert fh.read() == b""

test_read_directory_raises(root)

Source code in src/pathapi/testing/_read.py
205
206
207
208
def test_read_directory_raises(self, root):
    self._require("distinguishes_file_types")
    with pytest.raises(DIRECTORY_ERRORS):
        (root / "sub").read_bytes()

test_read_missing_raises_file_not_found(root)

Source code in src/pathapi/testing/_read.py
199
200
201
202
203
def test_read_missing_raises_file_not_found(self, root):
    with pytest.raises(FileNotFoundError):
        (root / "nonexistent.txt").read_bytes()
    with pytest.raises(FileNotFoundError):
        (root / "nonexistent" / "x.txt").read_text()

test_read_text_and_bytes(root)

Source code in src/pathapi/testing/_read.py
131
132
133
134
135
def test_read_text_and_bytes(self, root):
    assert (root / "a.txt").read_text() == "a"
    assert (root / "a.txt").read_bytes() == b"a"
    assert (root / "sub" / "c.py").read_text() == "c"
    assert (root / "sub" / "nested" / "d.py").read_bytes() == b"d"

test_stat(root)

Source code in src/pathapi/testing/_read.py
166
167
168
169
170
171
def test_stat(self, root):
    st = (root / "a.txt").stat()
    assert st.st_size == 1
    assert isinstance(st.st_mtime, (int, float))
    # A second, longer file: a backend that reports one size for all.
    assert (root / ".hidden.txt").stat().st_size == len("hidden")

test_stat_missing_raises_file_not_found(root)

Source code in src/pathapi/testing/_read.py
195
196
197
def test_stat_missing_raises_file_not_found(self, root):
    with pytest.raises(FileNotFoundError):
        (root / "nonexistent").stat()

test_stat_tells_a_directory_from_a_file(root)

Source code in src/pathapi/testing/_read.py
173
174
175
176
177
178
def test_stat_tells_a_directory_from_a_file(self, root):
    self._require("distinguishes_file_types")
    assert _stat.S_ISREG((root / "a.txt").stat().st_mode)
    assert not _stat.S_ISDIR((root / "a.txt").stat().st_mode)
    assert _stat.S_ISDIR((root / "sub").stat().st_mode)
    assert not _stat.S_ISREG((root / "sub").stat().st_mode)

test_walk(root)

Source code in src/pathapi/testing/_read.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def test_walk(self, root):
    self._require("supports_listing", root)
    expected = {
        ".": (["empty_dir", "sub"], [".hidden.txt", "a.txt", "b.py"]),
        "sub": (["nested"], ["c.py"]),
        "sub/nested": ([], ["d.py"]),
        "empty_dir": ([], []),
    }
    top_down = list(root.walk())
    actual = {
        _rel(root, d): (sorted(dirs), sorted(files)) for d, dirs, files in top_down
    }
    if not self.supports_empty_directories:
        del actual["empty_dir"], expected["empty_dir"]
    assert actual == expected
    assert _rel(root, top_down[0][0]) == "."

    bottom_up = [_rel(root, d) for d, _, _ in root.walk(top_down=False)]
    assert sorted(bottom_up) == sorted(_rel(root, d) for d, _, _ in top_down)
    assert bottom_up[-1] == "."
    assert bottom_up.index("sub/nested") < bottom_up.index("sub")

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/pathapi/testing/_fixtures.py
30
31
32
33
34
35
36
37
38
39
40
41
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

HTTP transport suite

Testing support, not part of the stable API: its contents may change in any release.

pathapi.http.testing

Testing support: the suite that holds every HTTP transport to the same promises.

This module is testing support, not part of the stable API: it is importable and documented, but its contents (the test names, the fixtures, the helper signatures, MODULES and HELPERS) may change in any release. pathapi.http never imports it.

HTTPClient and the transport contract are tested over a transport chosen by name, against loopback servers scripted at the byte level. A distribution that provides a transport (see pathapi.http.get_transport) runs the same tests over its own, with its distribution installed::

python -m pathapi.http.testing --http-transports requests -q

Everything after python -m pathapi.http.testing is passed to pytest, which this module needs. --http-transports takes names that pathapi.http.transport_names() lists, separated by commas; it defaults to builtin. Every request goes to the loopback interface: the suite fails a test that tries to reach any other host. The TLS tests need cryptography and skip without it. This module imports nothing but the standard library: importing it loads pathapi.http (and so ssl) but not pytest, which run() imports.

HELPERS = ('conftest.py', 'transport_fakes.py', 'transport_guard.py', 'transport_wire.py') module-attribute

The files beside them that they import: the fixtures and the loopback servers.

MODULES = ('test_client_default.py', 'test_client_errors.py', 'test_client_peer_text.py', 'test_client_redirect_origin.py', 'test_client_redirects.py', 'test_client_reply_limits.py', 'test_client_write_redirects.py', 'test_transport_body.py', 'test_transport_send.py') module-attribute

The test modules that run once per transport, by file name.

__all__ = ['HELPERS', 'MODULES', 'run', 'suite_directory'] module-attribute

run(arguments=None)

Runs MODULES with pytest and arguments (pytest options, such as --http-transports requests) and returns the exit status. Raises ImportError when pytest is not installed.

Source code in src/pathapi/http/testing/__init__.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def run(arguments: "_ty.Sequence[str] | None" = None) -> int:
    """Runs `MODULES` with pytest and `arguments` (pytest options, such as
    `--http-transports requests`) and returns the exit status. Raises
    `ImportError` when pytest is not installed."""
    try:
        import pytest
    except ImportError as error:
        raise ImportError(
            "the transport suite needs pytest: pip install pytest", name="pytest"
        ) from error
    directory = suite_directory()
    files = [str(directory / name) for name in MODULES]
    # The files go first: with an option and its value ahead of them, pytest
    # 8 reads the value as the first path and loads no `conftest.py`, so the
    # option `--http-transports` is not known.
    return int(pytest.main([*files, *(arguments or ())]))

suite_directory()

The directory that holds MODULES and HELPERS: the copy installed with the package, or tests/transport/ of a source checkout. Raises FileNotFoundError when neither is there.

Source code in src/pathapi/http/testing/__init__.py
54
55
56
57
58
59
60
61
62
def suite_directory() -> _pathlib.Path:
    """The directory that holds `MODULES` and `HELPERS`: the copy installed
    with the package, or `tests/transport/` of a source checkout. Raises
    `FileNotFoundError` when neither is there."""
    here = _pathlib.Path(__file__).resolve().parent
    for candidate in (here / "suite", here.parents[3] / "tests" / "transport"):
        if all((candidate / name).is_file() for name in (*MODULES, *HELPERS)):
            return candidate
    raise FileNotFoundError("the transport suite is not installed with pathapi")