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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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"
|
test_unlink(root)
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()
|
test_unlink_directory_raises(root)
Source code in src/pathapi/testing/_contract.py
| 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()
|
test_unlink_missing_raises_then_missing_ok(root)
Source code in src/pathapi/testing/_contract.py
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| def test_iterdir_missing_raises_file_not_found(self, root):
self._require("supports_listing", root)
with pytest.raises(FileNotFoundError):
list((root / "nonexistent").iterdir())
|
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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
|