pathapi 0.1.0__tar.gz → 0.1.2__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.
- {pathapi-0.1.0 → pathapi-0.1.2}/CHANGELOG.md +124 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/PKG-INFO +27 -9
- {pathapi-0.1.0 → pathapi-0.1.2}/README.md +26 -8
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/benchmarks.md +4 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/divergences.md +11 -3
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/index.md +0 -4
- {pathapi-0.1.0 → pathapi-0.1.2}/pyproject.toml +8 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/AGENTS.md +443 -45
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/__init__.py +7 -3
- pathapi-0.1.2/src/pathapi/_fanout.py +123 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_write.py +4 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/fspath.py +2 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_path.py +3 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/__init__.py +24 -4
- pathapi-0.1.2/src/pathapi/http/_async_client.py +212 -0
- pathapi-0.1.2/src/pathapi/http/_async_follow.py +130 -0
- pathapi-0.1.2/src/pathapi/http/_async_response.py +164 -0
- pathapi-0.1.2/src/pathapi/http/_async_transports.py +205 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_client.py +38 -5
- pathapi-0.1.2/src/pathapi/http/_deadline.py +96 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_follow.py +25 -3
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_headers.py +7 -2
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_proxies.py +4 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_transports.py +51 -43
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/testing/__init__.py +10 -2
- pathapi-0.1.2/src/pathapi/path/__init__.py +36 -0
- pathapi-0.1.2/src/pathapi/path/_async.py +509 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_path.py +5 -0
- pathapi-0.1.2/src/pathapi/protocols/_copytree.py +160 -0
- pathapi-0.1.2/src/pathapi/protocols/_moving.py +154 -0
- pathapi-0.1.2/src/pathapi/protocols/_removing.py +141 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_transfer.py +9 -0
- pathapi-0.1.2/src/pathapi/protocols/_writing.py +153 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/transfer.py +118 -145
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/tree.py +65 -107
- pathapi-0.1.2/src/pathapi/sync/_entry.py +567 -0
- pathapi-0.1.2/src/pathapi/sync/_syncer.py +529 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/__init__.py +25 -0
- pathapi-0.1.2/src/pathapi/testing/_async.py +353 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_contract.py +42 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_read.py +38 -24
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/mutants.py +435 -59
- pathapi-0.1.2/tests/test_async_contract.py +104 -0
- pathapi-0.1.2/tests/test_async_path.py +497 -0
- pathapi-0.1.2/tests/test_copy_from_names.py +83 -0
- pathapi-0.1.2/tests/test_copy_overwrite.py +141 -0
- pathapi-0.1.2/tests/test_copy_symlink_overwrite.py +188 -0
- pathapi-0.1.2/tests/test_fanout.py +444 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_end_to_end.py +27 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_http_testing.py +64 -1
- pathapi-0.1.2/tests/test_move_overwrite_secures.py +352 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_packaging.py +4 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_public_surface.py +13 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_shipped_header.py +7 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/conftest.py +44 -0
- pathapi-0.1.2/tests/transport/test_async_client.py +278 -0
- pathapi-0.1.2/tests/transport/test_async_registry.py +290 -0
- pathapi-0.1.2/tests/transport/test_async_transport.py +486 -0
- pathapi-0.1.2/tests/transport/test_async_units.py +415 -0
- pathapi-0.1.2/tests/transport/test_client_deadline.py +184 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_default.py +2 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_prepare.py +14 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_proxy.py +3 -2
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_registry.py +49 -18
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_send.py +14 -1
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_fakes.py +50 -1
- pathapi-0.1.0/src/pathapi/path/__init__.py +0 -19
- pathapi-0.1.0/src/pathapi/sync/_syncer.py +0 -989
- {pathapi-0.1.0 → pathapi-0.1.2}/.gitignore +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/LICENSE +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/archive.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/checksum.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/exceptions.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/ftp.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/glob.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/http.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/mempath.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/path.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/protocols.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/providers.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/sync.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/testing.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/changelog.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/docs/guides/extending.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/examples/archive_members.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/examples/ftp_listing.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/examples/http_request.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/examples/local_and_mem.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/mkdocs.yml +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_commit.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_discard.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_errorpolicy.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_filestat.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_lru.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_names.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_permissions.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_stubs.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_tree.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/__init__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_backend.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_names.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_pack.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_path.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_snapshot.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_tar.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_zip.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_zipentries.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/checksum.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/exceptions.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/__init__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_backend.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_facts.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/__init__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_entries.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_patterns.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_recursion.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_selection.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_body.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_builtin.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_errors.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_pool.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_response.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_settings.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_status.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/testing/__main__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/mempath.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_info.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_pure.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_relation.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_transfer.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/__init__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_bundles.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_errors.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_info.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_relation.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_removal.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_support.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/checksum.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/fs.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/io.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/links.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/pure.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/resolve.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/providers.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/py.typed +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/__init__.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_compare.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_events.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_links.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_offer.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_paths.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_replace.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_fixtures.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/capabilities.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/census.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/conftest.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/ftp_loopback.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/hermetic.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/README.md +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/mutation_check.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/run.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_environment_leak.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_leaks_a_socket.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_session_environment.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_batch.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_batch_cost.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_damage.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_helpers.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_lazy.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_link_listing.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_links.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_names.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_parity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_path.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_rewrite.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_safety.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_scale.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_tree.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_census.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_checksum.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_commit_on_close.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_composed_protocols.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_consumer_needs.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_contract.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_contract_helpers.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_copy_move_fidelity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_copy_round_trips.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_destructive_safety.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_documented_examples.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_error_predicate.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_examples_skip.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_exceptions.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_filestat.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_path.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_security.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_utime.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_wire.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_generic_pure_parity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob_edges.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob_parity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_hermetic.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_io_parity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_listing_hints.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_listing_names.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_local.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_local_stat_errors.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_low_core.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_low_sync.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_mempath.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_move_stats.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_mro_precedence.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_parity_io.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_parity_pure.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_gaps.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_info.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_moving.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_pathname.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_properties.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_protocol_composition.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_protocol_support.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_provider_seam.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_providers.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_pure_parity.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_resolve.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_same_filesystem.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_smoke.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_symlink_target.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_cost.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_mtime.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_safety.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_walk.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_walk_junction.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_write_discard.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_errors.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_peer_text.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_redirect_origin.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_redirects.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_reply_limits.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_urls.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_write_redirects.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_body.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_pool.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_tls.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_distribution.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_guard.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_wire.py +0 -0
- {pathapi-0.1.0 → pathapi-0.1.2}/tests/waits.py +0 -0
|
@@ -7,6 +7,127 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.2] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`max_workers` for recursive `copy()`, `rm()` and `PathSyncer`.** `copy()` (and
|
|
15
|
+
`copy_into()`), `rm()` and `PathSyncer(...)` take `max_workers=None`. `None`, the
|
|
16
|
+
default, is one thread, exactly as before. An int of 2 or more hands the files
|
|
17
|
+
and links of each directory to a pool of at most that many threads, which
|
|
18
|
+
helps a backend that pays a round trip per entry. It applies to a class that sets
|
|
19
|
+
the new class attribute `_threadsafe = True` (`LocalPath` and `FTPPath` do); any other
|
|
20
|
+
class, including every class of another distribution until it sets the attribute,
|
|
21
|
+
runs serially whatever was asked. Callbacks (`progress`, `on_error`, `ignore_error`,
|
|
22
|
+
the sync `hook`, follow policies) run one at a time but on pool threads, in no
|
|
23
|
+
fixed order, and the error that ends the operation is the first to happen. A value
|
|
24
|
+
below 1 is `ValueError`, one that is not an int is `TypeError`. `PathContract`
|
|
25
|
+
has a `supports_concurrent_use` switch for the matching test.
|
|
26
|
+
- **`AsyncPath` and `AsyncFile`: a path for use from an event loop.** `AsyncPath(path)`
|
|
27
|
+
wraps any `Path`; the pure members (`name`, `parent`, `/`, `with_*()`, `match()`, ...)
|
|
28
|
+
are plain and give `AsyncPath`, every method that does I/O is a coroutine of the same
|
|
29
|
+
name and arguments, `iterdir()`, `glob()`, `rglob()` and `walk()` return async iterators,
|
|
30
|
+
and `open()` returns an object that is awaited or used with `async with` and gives an
|
|
31
|
+
`AsyncFile` (`read`, `readline`, `write`, `seek`, ..., `aclose()`). They are exported
|
|
32
|
+
from `pathapi` and `pathapi.path`. Each call runs the wrapped path's sync method on a
|
|
33
|
+
worker thread (`asyncio.to_thread()`), so every backend can be used from a loop and the
|
|
34
|
+
loop is not blocked; cancelling an `await` abandons the wait and the worker finishes the
|
|
35
|
+
call. Recursive `copy()`, `rm()` and `move()` are one such call, so `max_workers=` is the
|
|
36
|
+
sync fan-out. `supports()` and `capabilities()` of an `AsyncPath` answer for the wrapped
|
|
37
|
+
path. A subclass gets its own class for every path derived from it, built by the
|
|
38
|
+
classmethod `AsyncPath._adopt(path)` (`cls(path)`), which a subclass with another
|
|
39
|
+
constructor overrides.
|
|
40
|
+
- **`AsyncHTTPClient`: the HTTP client for an event loop.** `pathapi.http.AsyncHTTPClient`
|
|
41
|
+
is the sibling of `HTTPClient`: the same constructor arguments, redirect and
|
|
42
|
+
`Authorization` rules, `timeout`, `deadline`, reply limit and errors, with `request()`
|
|
43
|
+
and `redirect()` as coroutines, `aclose()` and `async with`. It returns an
|
|
44
|
+
`AsyncHTTPResponse` (`await read()`, `iter_bytes()` as an async iterator, `await
|
|
45
|
+
aclose()`). It sends through an async transport, an object with `async send()` and
|
|
46
|
+
`async aclose()`. The default one, `ThreadedTransport`, runs the synchronous transport
|
|
47
|
+
`PATHAPI_HTTP_TRANSPORT` names on worker threads, so every synchronous transport works
|
|
48
|
+
from a loop; a native one is declared in the entry-point group
|
|
49
|
+
`pathapi.http_async_transports` and chosen by `PATHAPI_HTTP_ASYNC_TRANSPORT` (same
|
|
50
|
+
rules and messages as the synchronous registry; `async_transport_names()`,
|
|
51
|
+
`get_async_transport()` and `default_async_transport()`).
|
|
52
|
+
- **The async half of the transport suite.** `python -m pathapi.http.testing
|
|
53
|
+
--http-async-transports NAME` holds an async transport to the promises of the
|
|
54
|
+
synchronous suite (`test_async_transport.py`, `test_async_client.py`). Without the
|
|
55
|
+
option the async tests run over `builtin` when `--http-transports` is left out too,
|
|
56
|
+
and are not collected when it names a transport: a distribution that runs
|
|
57
|
+
`--http-transports NAME` gets the tests it got before.
|
|
58
|
+
- **`pathapi.testing.AsyncPathContract`.** The pytest contract of `AsyncPath` over a
|
|
59
|
+
backend: it takes the `root` fixture of `PathContract` and checks each coroutine
|
|
60
|
+
against the sync path's result, with `asyncio.run()` and no extra test dependency. Its
|
|
61
|
+
class attribute `async_path_class` (default `AsyncPath`) names the class under test, so a
|
|
62
|
+
subclass of `AsyncPath` is tested with the paths it derives.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **An unknown HTTP transport name is told where to look, not which package to
|
|
67
|
+
install.** `get_transport()` and `PATHAPI_HTTP_TRANSPORT` raise `ValueError` for a
|
|
68
|
+
name no installed distribution declares in the group `pathapi.http_transports`,
|
|
69
|
+
naming the group and suggesting `pathapi-NAME` as a name to look for. The error is
|
|
70
|
+
a `ValueError` that is also an `ImportError`, so code that caught either still
|
|
71
|
+
does. `requests` is no longer special: `transport_names()` lists it only when an
|
|
72
|
+
installed distribution declares it. The refusal of a proxy that is not `http://`
|
|
73
|
+
points at a transport of that group that supports it. Install the distribution
|
|
74
|
+
that provides the transport.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- **A `nan` or infinite HTTP timeout is refused with `ValueError`.** `timeout=float("nan")`
|
|
79
|
+
passed the check, and the built-in transport then failed in `socket.settimeout()` after
|
|
80
|
+
it had opened the connection, which leaked the socket. Pass a positive finite number of
|
|
81
|
+
seconds, or `None`.
|
|
82
|
+
- **`move(overwrite=True)` no longer destroys the old target when the move fails.**
|
|
83
|
+
An existing target was removed before the source was renamed or copied over it, so
|
|
84
|
+
a failing rename or copy lost the target. The target now stays until the source
|
|
85
|
+
has taken its place: a local file is replaced atomically, `rename()` is tried
|
|
86
|
+
over a file elsewhere, a backend that refuses has the target renamed aside and put
|
|
87
|
+
back on failure, and a copy and delete finishes the new file or tree beside the
|
|
88
|
+
target first. A directory target is swapped in by rename where its class has one.
|
|
89
|
+
If you list the directory while a move runs, you may see a hidden
|
|
90
|
+
`.NAME.<hex>.pathapi-tmp` sibling.
|
|
91
|
+
- **`copy(follow_symlinks=False, overwrite=True)` of a link no longer destroys the old
|
|
92
|
+
target when the new link cannot be made.** The existing target was unlinked before
|
|
93
|
+
`symlink_to()`, so a refused link lost it. Where the target's class can rename, the
|
|
94
|
+
link is now made in a hidden sibling and renamed over the target; a class that
|
|
95
|
+
cannot rename still unlinks first. A target class that cannot make links now raises
|
|
96
|
+
`NotImplementedError` before anything is removed.
|
|
97
|
+
|
|
98
|
+
## [0.1.1] - 2026-10-16
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- **`deadline` for a whole HTTP request.** `HTTPClient(deadline=...)` and the
|
|
103
|
+
`deadline=` keyword of `request()` and `redirect()` take the seconds a request may
|
|
104
|
+
take, redirects and the reading of the body included (`None`, the default, is no
|
|
105
|
+
limit). Each wait's timeout is held to the time that remains and `TimeoutError` is
|
|
106
|
+
raised once it has passed; `timeout` still bounds each wait. A read already blocked
|
|
107
|
+
can overrun the deadline by at most its timeout. A value that is not a number is
|
|
108
|
+
`TypeError`, one that is not positive `ValueError`. It works over every transport.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **`copy(overwrite=True)` no longer destroys the old target when the copy fails.**
|
|
113
|
+
An existing file was removed before the new content was written, so a source that
|
|
114
|
+
failed half way left no file, or a partial one. The old file is kept until the
|
|
115
|
+
new content is complete: it is written to a hidden `.NAME.<hex>.pathapi-tmp`
|
|
116
|
+
sibling and renamed over it, and a class whose write stream commits on close is
|
|
117
|
+
written to directly. A class that cannot rename writes the sibling and then writes
|
|
118
|
+
it over the target. If you list the directory while a copy runs, you may see that
|
|
119
|
+
sibling.
|
|
120
|
+
- **`pathlib.Path(src).copy(target)` (3.14) onto a `Path` of this library refuses
|
|
121
|
+
unsafe child names.** `_copy_from()` joined every listed name onto the target; a
|
|
122
|
+
name that is not one component (`..`, a separator, on a Windows target a drive or
|
|
123
|
+
a trailing dot) now raises `ValueError` before anything is written for it, as
|
|
124
|
+
`copy(recursive=True)` does. A source that lists such names needs a copy that
|
|
125
|
+
skips them.
|
|
126
|
+
- **The shipped transport suite names the file of its tests wherever it is run
|
|
127
|
+
from.** `python -m pathapi.http.testing` (and `run()`) roots pytest at the suite's
|
|
128
|
+
own directory unless `--rootdir` is given, so node ids keep their file when the
|
|
129
|
+
checkout and the installed package are on different drives.
|
|
130
|
+
|
|
10
131
|
## [0.1.0] - 2026-10-15
|
|
11
132
|
|
|
12
133
|
Published as `pathlib-next` up to 0.10.0; from 0.1.0 as `pathapi`. The released
|
|
@@ -3539,7 +3660,9 @@ behaviour with what to do about it.
|
|
|
3539
3660
|
- Sync error handling.
|
|
3540
3661
|
- Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
|
|
3541
3662
|
|
|
3542
|
-
[Unreleased]: https://github.com/jose-pr/pathapi/compare/v0.1.
|
|
3663
|
+
[Unreleased]: https://github.com/jose-pr/pathapi/compare/v0.1.2...HEAD
|
|
3664
|
+
[0.1.2]: https://github.com/jose-pr/pathapi/compare/v0.1.1...v0.1.2
|
|
3665
|
+
[0.1.1]: https://github.com/jose-pr/pathapi/compare/v0.1.0...v0.1.1
|
|
3543
3666
|
[0.1.0]: https://github.com/jose-pr/pathapi/compare/v0.10.0...v0.1.0
|
|
3544
3667
|
[0.10.0]: https://github.com/jose-pr/pathlib-next/compare/v0.9.14...v0.10.0
|
|
3545
3668
|
[0.9.14]: https://github.com/jose-pr/pathlib-next/compare/v0.9.13...v0.9.14
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pathapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Generic Path Protocol based pathlib
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/pathapi/
|
|
6
6
|
Project-URL: Documentation, https://jose-pr.github.io/pathapi/
|
|
@@ -78,8 +78,10 @@ where the backend has a faster way.
|
|
|
78
78
|
- **`pathapi.http`**: `HTTPClient` and a transport contract for HTTP
|
|
79
79
|
requests on the standard library (redirects that never carry
|
|
80
80
|
`Authorization` to another origin, TLS verified by default, a `proxies=`
|
|
81
|
-
provider);
|
|
82
|
-
`pathapi-
|
|
81
|
+
provider); another HTTP library plugs in as a transport through the
|
|
82
|
+
`pathapi.http_transports` entry-point group. `AsyncHTTPClient` is the same
|
|
83
|
+
client for an event loop, over async transports registered in
|
|
84
|
+
`pathapi.http_async_transports`.
|
|
83
85
|
- **`PathSyncer`**: one-way, content-compared tree sync between any two
|
|
84
86
|
implementations, with dry runs and event hooks.
|
|
85
87
|
- **Extensible**: subclass `Path` for any resource, and verify it with the
|
|
@@ -90,10 +92,6 @@ Paths for other places are separate distributions built on this one:
|
|
|
90
92
|
| Distribution | Provides |
|
|
91
93
|
| --- | --- |
|
|
92
94
|
| [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI, with the schemes `file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:` and `dav:`, and the `uripath` command |
|
|
93
|
-
| [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
|
|
94
|
-
| [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` (Amazon S3, Google Cloud Storage, Azure Blob Storage) |
|
|
95
|
-
| [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` (the REST APIs of the hosting services) |
|
|
96
|
-
| [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport`, the HTTP transport that sends through a `requests.Session` |
|
|
97
95
|
|
|
98
96
|
## Installation
|
|
99
97
|
|
|
@@ -178,7 +176,7 @@ print(sorted(p.name for p in Path("site-copy").iterdir()))
|
|
|
178
176
|
| `pathapi.mempath` | `MemPath` in-memory filesystem |
|
|
179
177
|
| `pathapi.ftp` | `FTPPath`, `FTPBackend` (FTP and FTPS on `ftplib`) |
|
|
180
178
|
| `pathapi.archive` | `ZipPath`, `TarPath`, `ArchivePath` (members of a zip or tar file), `make_archive`, `unpack_archive` |
|
|
181
|
-
| `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse` (HTTP requests on `http.client
|
|
179
|
+
| `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse`, `AsyncHTTPClient`, `ThreadedTransport` (HTTP requests on `http.client`, and from an event loop) |
|
|
182
180
|
| `pathapi.protocols` | `Stat`, `Chmod`, `BinaryOpen`, `NativeChecksum` |
|
|
183
181
|
| `pathapi.sync` | `PathSyncer`, `SyncEvent`, `PathAndStat` |
|
|
184
182
|
| `pathapi.glob` | the glob engine behind `Path.glob()` |
|
|
@@ -192,13 +190,33 @@ The full reference is on the [documentation site](https://jose-pr.github.io/path
|
|
|
192
190
|
an agent-oriented API summary ships inside the package as
|
|
193
191
|
`pathapi/AGENTS.md`.
|
|
194
192
|
|
|
193
|
+
## Async
|
|
194
|
+
|
|
195
|
+
`AsyncPath(path)` wraps any `pathapi` path for use inside an event loop: the
|
|
196
|
+
pure members stay plain, every method that does I/O is a coroutine of the same
|
|
197
|
+
name, listings are async iterators and `open()` gives an `AsyncFile`. Each call
|
|
198
|
+
runs the sync method on a worker thread, so every backend works at once and the
|
|
199
|
+
loop is never blocked; it is not a native async client.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import asyncio
|
|
203
|
+
from pathapi import AsyncPath, LocalPath
|
|
204
|
+
|
|
205
|
+
async def main():
|
|
206
|
+
root = AsyncPath(LocalPath("."))
|
|
207
|
+
async for entry in root.iterdir():
|
|
208
|
+
print(entry.name, await entry.is_dir())
|
|
209
|
+
|
|
210
|
+
asyncio.run(main())
|
|
211
|
+
```
|
|
212
|
+
|
|
195
213
|
## Extending
|
|
196
214
|
|
|
197
215
|
Subclass `Path` for a resource of your own (`MemPath` is the reference), as
|
|
198
216
|
worked through in
|
|
199
217
|
[Extending](https://jose-pr.github.io/pathapi/guides/extending/).
|
|
200
218
|
`pathapi.testing` provides the pytest contracts (`PurePathContract`,
|
|
201
|
-
`ReadPathContract`, `PathContract`) and `populate_fixture_tree()` that every
|
|
219
|
+
`ReadPathContract`, `PathContract`, `AsyncPathContract`) and `populate_fixture_tree()` that every
|
|
202
220
|
class of the package and of the distributions above is verified with.
|
|
203
221
|
|
|
204
222
|
## Supported Python versions
|
|
@@ -41,8 +41,10 @@ where the backend has a faster way.
|
|
|
41
41
|
- **`pathapi.http`**: `HTTPClient` and a transport contract for HTTP
|
|
42
42
|
requests on the standard library (redirects that never carry
|
|
43
43
|
`Authorization` to another origin, TLS verified by default, a `proxies=`
|
|
44
|
-
provider);
|
|
45
|
-
`pathapi-
|
|
44
|
+
provider); another HTTP library plugs in as a transport through the
|
|
45
|
+
`pathapi.http_transports` entry-point group. `AsyncHTTPClient` is the same
|
|
46
|
+
client for an event loop, over async transports registered in
|
|
47
|
+
`pathapi.http_async_transports`.
|
|
46
48
|
- **`PathSyncer`**: one-way, content-compared tree sync between any two
|
|
47
49
|
implementations, with dry runs and event hooks.
|
|
48
50
|
- **Extensible**: subclass `Path` for any resource, and verify it with the
|
|
@@ -53,10 +55,6 @@ Paths for other places are separate distributions built on this one:
|
|
|
53
55
|
| Distribution | Provides |
|
|
54
56
|
| --- | --- |
|
|
55
57
|
| [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI, with the schemes `file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:` and `dav:`, and the `uripath` command |
|
|
56
|
-
| [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
|
|
57
|
-
| [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` (Amazon S3, Google Cloud Storage, Azure Blob Storage) |
|
|
58
|
-
| [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` (the REST APIs of the hosting services) |
|
|
59
|
-
| [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport`, the HTTP transport that sends through a `requests.Session` |
|
|
60
58
|
|
|
61
59
|
## Installation
|
|
62
60
|
|
|
@@ -141,7 +139,7 @@ print(sorted(p.name for p in Path("site-copy").iterdir()))
|
|
|
141
139
|
| `pathapi.mempath` | `MemPath` in-memory filesystem |
|
|
142
140
|
| `pathapi.ftp` | `FTPPath`, `FTPBackend` (FTP and FTPS on `ftplib`) |
|
|
143
141
|
| `pathapi.archive` | `ZipPath`, `TarPath`, `ArchivePath` (members of a zip or tar file), `make_archive`, `unpack_archive` |
|
|
144
|
-
| `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse` (HTTP requests on `http.client
|
|
142
|
+
| `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse`, `AsyncHTTPClient`, `ThreadedTransport` (HTTP requests on `http.client`, and from an event loop) |
|
|
145
143
|
| `pathapi.protocols` | `Stat`, `Chmod`, `BinaryOpen`, `NativeChecksum` |
|
|
146
144
|
| `pathapi.sync` | `PathSyncer`, `SyncEvent`, `PathAndStat` |
|
|
147
145
|
| `pathapi.glob` | the glob engine behind `Path.glob()` |
|
|
@@ -155,13 +153,33 @@ The full reference is on the [documentation site](https://jose-pr.github.io/path
|
|
|
155
153
|
an agent-oriented API summary ships inside the package as
|
|
156
154
|
`pathapi/AGENTS.md`.
|
|
157
155
|
|
|
156
|
+
## Async
|
|
157
|
+
|
|
158
|
+
`AsyncPath(path)` wraps any `pathapi` path for use inside an event loop: the
|
|
159
|
+
pure members stay plain, every method that does I/O is a coroutine of the same
|
|
160
|
+
name, listings are async iterators and `open()` gives an `AsyncFile`. Each call
|
|
161
|
+
runs the sync method on a worker thread, so every backend works at once and the
|
|
162
|
+
loop is never blocked; it is not a native async client.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import asyncio
|
|
166
|
+
from pathapi import AsyncPath, LocalPath
|
|
167
|
+
|
|
168
|
+
async def main():
|
|
169
|
+
root = AsyncPath(LocalPath("."))
|
|
170
|
+
async for entry in root.iterdir():
|
|
171
|
+
print(entry.name, await entry.is_dir())
|
|
172
|
+
|
|
173
|
+
asyncio.run(main())
|
|
174
|
+
```
|
|
175
|
+
|
|
158
176
|
## Extending
|
|
159
177
|
|
|
160
178
|
Subclass `Path` for a resource of your own (`MemPath` is the reference), as
|
|
161
179
|
worked through in
|
|
162
180
|
[Extending](https://jose-pr.github.io/pathapi/guides/extending/).
|
|
163
181
|
`pathapi.testing` provides the pytest contracts (`PurePathContract`,
|
|
164
|
-
`ReadPathContract`, `PathContract`) and `populate_fixture_tree()` that every
|
|
182
|
+
`ReadPathContract`, `PathContract`, `AsyncPathContract`) and `populate_fixture_tree()` that every
|
|
165
183
|
class of the package and of the distributions above is verified with.
|
|
166
184
|
|
|
167
185
|
## Supported Python versions
|
|
@@ -17,6 +17,7 @@ Narrower suites:
|
|
|
17
17
|
```bash
|
|
18
18
|
python benchmarks/bench.py syncer
|
|
19
19
|
python benchmarks/bench.py recursive-matrix
|
|
20
|
+
python benchmarks/bench.py fanout
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
## Saved Results
|
|
@@ -47,6 +48,9 @@ claims in release notes come from CI runs.
|
|
|
47
48
|
- `LocalPath` vs `pathlib.Path` on common local operations
|
|
48
49
|
- `PathSyncer` on local trees, and recursive copy/remove on local and
|
|
49
50
|
in-memory trees
|
|
51
|
+
- the `fanout` suite: recursive copy, remove and a first sync of a tree of
|
|
52
|
+
small files on a loopback FTP server and on a simulated slow backend (a
|
|
53
|
+
`MemPath` that waits 2 ms per call, not a measurement of a real one)
|
|
50
54
|
|
|
51
55
|
The benchmarks of the classes in other distributions (URIs, HTTP listings,
|
|
52
56
|
SFTP, object stores) live with those distributions.
|
|
@@ -70,8 +70,8 @@ these operations itself always keeps its own implementation.
|
|
|
70
70
|
| `FTPPath.stat().st_mode` | pathlib reports the real mode | Taken from MLSD `unix.mode`, else derived from the `perm` fact (read/write bits only). | MLSD is the only mode source most servers offer. |
|
|
71
71
|
| `Path.touch(mode=None, exist_ok=True)` on non-local backends | `pathlib.Path.touch(mode=0o666)` creates with the mode masked by the umask and updates an existing file's mtime | `mode` defaults to `None`: a new file keeps the backend's default permissions, and an explicit `mode` is applied with `chmod()` unmasked (a remote umask is unknown); a class that cannot `chmod()` raises `NotImplementedError` and removes the file it created, so a mode that was asked for is never silently dropped. An existing file's access and modification times are set to now through the overridable `_utime()` hook (`MemPath` and `FTPPath` on a server that lists `MFMT` implement it: whole seconds, no access time on FTP); a class that cannot set a time leaves the file as it is. `LocalPath` uses pathlib's `touch()` unchanged. | Always chmod'ing 0o666 left new files world-writable on FTP and other remote backends; a remote backend cannot apply the local umask. |
|
|
72
72
|
| `Path.__iter__` | `pathlib.Path` is not iterable (no `__iter__`) | `iter(path)` is `path.iterdir()` | Deliberate extension for ergonomic `for child in path:` loops. **Caution:** on remote backends (FTP) this is a network call. **User decision, 2026-07-11.** |
|
|
73
|
-
| `Path.copy(target, ...)` | CPython 3.14 `Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)`: overwrites an existing file, copies a directory tree without being asked, returns the new path | Ours predates 3.14. Signature: `copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)`; returns `None`. `ignore_error=` keeps its notify-only callable, and `on_error=` is `rm()`'s predicate `(error, path) -> bool` for a child error of a recursive copy (`True` skips the entry, `False` raises, a predicate that raises ends the copy with its own exception); passing both raises `TypeError`. An existing target raises `FileExistsError` unless `overwrite=True`; a directory needs `recursive=True`. `overwrite=True`
|
|
74
|
-
| `Path.move(target, ...)` | CPython 3.14 `Path.move(target)`: `os.replace` semantics (replaces an existing file), falls back to copy + delete across filesystems, returns the new path | Ours predates 3.14 and keeps `overwrite=False`: an existing target raises `FileExistsError`. Returns whatever `rename()` returns (`None` on the copy fallback). Our own extension: tries `rename()` (by default only onto a path of the same class; `LocalPath` and classes for URIs decide for themselves), falls back to copy+unlink, which recreates a symlink and the links inside a moved tree wherever the target can hold links, so the result does not depend on whether `rename()` was available. Validates before touching the target: a missing source raises `FileNotFoundError`, a file onto a directory raises `IsADirectoryError`, a target that holds the source (`OSError(ENOTEMPTY)`), lies inside it (`OSError(EINVAL)`) or is a link to or from the same file (`OSError(EINVAL)`) is refused with both trees intact, and the same file under another spelling (a case-only rename, another class, a relative `MemPath`, another spelling of the same archive) is renamed in place. "Same" is decided as for `copy()`. Two hard links of one file are two names: an existing target needs `overwrite=True`, and the source name is then removed. `overwrite=True` replaces a local file
|
|
73
|
+
| `Path.copy(target, ...)` | CPython 3.14 `Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)`: overwrites an existing file, copies a directory tree without being asked, returns the new path | Ours predates 3.14. Signature: `copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)`; returns `None`. `ignore_error=` keeps its notify-only callable, and `on_error=` is `rm()`'s predicate `(error, path) -> bool` for a child error of a recursive copy (`True` skips the entry, `False` raises, a predicate that raises ends the copy with its own exception); passing both raises `TypeError`. An existing target raises `FileExistsError` unless `overwrite=True`; a directory needs `recursive=True`. `overwrite=True` never removes or truncates the old file before the new content is secured: a missing, unreadable or half-read source leaves the target as it was, and a new file whose copy fails is removed. The new content is finished in a hidden temporary sibling and renamed over the old file (`os.replace` for a local path); a class that cannot rename writes the temporary sibling and then writes it over the target, which truncates it in place for the moment of that second write (the temporary file is kept and named if that fails), and a class whose write stream commits on close is written to directly, since a failed write leaves the old file. 3.14's `Path.copy()` truncates an existing target in place; copying onto the same file, or a directory into itself, raises `OSError(EINVAL)`: two files of this machine (`LocalPath`, a subclass, `pathlib.Path`, a class that names a local file through `filepath`) are compared by identity whatever their classes, a type that answers the overridable `_node_key()` hook (`MemPath`, archive members) by its normalized node, and anything else by `samefile()` where `stat()` has `st_dev`/`st_ino`, otherwise equal paths of one type that the overridable `_same_filesystem()` hook places on the same host or store (two paths of one class that name one server, built separately, count as the same file); `preserve_metadata` defaults to **True** (opposite of 3.14) and only propagates `st_mode`, not timestamps/xattrs: the permission bits of a file, and of each directory of a recursive copy once its children are copied (as `shutil.copytree` and 3.14 do), with the setuid, setgid and sticky bits dropped when source and target are different classes; a recreated symlink (`follow_symlinks=False`) carries only its target text, not its own mode, times or owner, and with `overwrite=True` it is made in a hidden sibling and renamed over the old entry where the class can rename (a failed link leaves the old entry), while a class that cannot rename unlinks the old entry first and so loses it if the new link then fails; a dangling symlink at the target is written through, as `shutil.copyfile` and 3.14 do, with or without `overwrite`, while a live one counts as existing and `overwrite=True` replaces the link itself | Argument names aligned with 3.14 where cheap; `preserve_metadata=True` default kept for backward compat with this method's pre-existing (pre-3.14-alignment) behavior of always copying the mode bits. Full metadata preservation (timestamps, xattrs) is not implemented. |
|
|
74
|
+
| `Path.move(target, ...)` | CPython 3.14 `Path.move(target)`: `os.replace` semantics (replaces an existing file), falls back to copy + delete across filesystems, returns the new path | Ours predates 3.14 and keeps `overwrite=False`: an existing target raises `FileExistsError`. Returns whatever `rename()` returns (`None` on the copy fallback). Our own extension: tries `rename()` (by default only onto a path of the same class; `LocalPath` and classes for URIs decide for themselves), falls back to copy+unlink, which recreates a symlink and the links inside a moved tree wherever the target can hold links, so the result does not depend on whether `rename()` was available. Validates before touching the target: a missing source raises `FileNotFoundError`, a file onto a directory raises `IsADirectoryError`, a target that holds the source (`OSError(ENOTEMPTY)`), lies inside it (`OSError(EINVAL)`) or is a link to or from the same file (`OSError(EINVAL)`) is refused with both trees intact, and the same file under another spelling (a case-only rename, another class, a relative `MemPath`, another spelling of the same archive) is renamed in place. "Same" is decided as for `copy()`. Two hard links of one file are two names: an existing target needs `overwrite=True`, and the source name is then removed. `overwrite=True` keeps an existing target until what replaces it is in place: a local file is replaced atomically (`os.replace`); elsewhere the source is renamed over it, or, where the backend refuses, the target is renamed aside, the source renamed in and the target put back if that fails; a copy and delete finishes the new file or tree beside the target first (a directory target is swapped in by rename where its class has one, and otherwise removed only after the copy beside it is complete). A hidden `.NAME.<hex>.pathapi-tmp` sibling exists meanwhile. | N/A -- pure extension, no pathlib method to diverge from. Removing the target before checking the source deleted it on a typo or a locked source. |
|
|
75
75
|
| `Path.is_junction()` / `is_mount()` / `is_dir_binding()` | `pathlib` has `is_junction()` (3.12+) and `is_mount()`, but only for local paths and with no shared concept | Both are part of the `Path` contract (False by default, answered by `LocalPath`), and `is_dir_binding()` is their union: a directory that is another tree's second NAME rather than part of this one. A junction is the Windows spelling of a bind mount, and neither is a symlink -- `is_symlink()` is False and a non-following stat reports a plain directory. | Walking code needs the distinction: a symlink announces itself and every walker here already declines to follow one, while a binding looks exactly like an ordinary directory and its contents belong to whoever mounted or junctioned it. |
|
|
76
76
|
| `Path.rm(recursive=, missing_ok=, ignore_error=)` | Not in `pathlib` (closest: `shutil.rmtree`) | Our own extension. Recursive removal deletes bottom-up and uses non-following stat/listing metadata, so a symlink to a directory -- or a binding (`is_dir_binding()`: a Windows junction, a mount point), which a non-following stat still reports as a directory -- is unlinked or `rmdir()`'d rather than traversed -- `rm -r` semantics. `follow_symlinks=`/`follow_binds=` select that per call: `False` (default), `True` (remove what is behind it, then the entry: a followed symlink is unlinked), `None` (leave it), or a callable asked per entry -- the same keyword `stat()`/`walk()`/`copy()` use for the same idea. They decide the path `rm()` is called on as they decide an entry met on the way down. A policy that is not one of those, or a callable's answer that is not, raises `ValueError` and is never offered to `ignore_error`. Every class that overrides `rm()` takes both keywords; a backend with no symlinks or bindings decides nothing with them, and one with a concurrent native walk runs it only for `follow_symlinks=False`. `rmdir()` on a live mount fails, which is the intended answer. | N/A -- pure extension. Non-following recursive deletion avoids deleting through symlinked directory targets and lets backends with metadata-rich listings remove trees without a stat round trip per child. |
|
|
77
77
|
| `Path.symlink_to(target, target_is_directory=False, *, force=False)` | `pathlib.Path.symlink_to(target, target_is_directory=False)` -- raises `FileExistsError` if anything already exists at the link path | Adds a keyword-only `force=`. `force=False` (the default) is stdlib-exact. `force=True` unlinks an existing **non-directory** entry at the link path first, then creates the symlink; an existing *directory* is never removed and the underlying error propagates. Not atomic: no filesystem or transport offers "replace a symlink" as one operation, so the path briefly does not exist between the unlink and the symlink. | Additive extension (an extra optional kwarg, per the parity contract). No backend can offer this atomically, so every consumer was re-implementing the same unlink-then-symlink dance -- it is path semantics, not transport semantics, so it belongs at the `Path` layer where one implementation serves every backend. Backends implement only the `_symlink_to()` primitive (same `_mkdir`/`_open` shape) and get `force=` for free. Because no stdlib version accepts the keyword, `symlink_to` is in `_OPERATION_NAMES` so `LocalPath` honors it too. |
|
|
@@ -94,7 +94,7 @@ these operations itself always keeps its own implementation.
|
|
|
94
94
|
| `FTPPath`, `ZipPath`, `TarPath`, `ArchivePath`: `==`, rooted and relative spellings | `PurePosixPath("a") != PurePosixPath("/a")`, and equal paths are the same file because there is one filesystem | `==` and `hash` compare the names only (as `MemPath` does), so two paths of different servers or archives are equal; `copy()`, `move()` and `PathSyncer` ask the overridable `_same_filesystem()` and `_node_key()` hooks before they call two equal paths one file. `a/b` and `/a/b` are two spellings of one node, because the tree has no working directory and is read from its root: the server root for an `FTPPath`, the archive root for the archive paths (`_node_key()` is equal; `==` is not). An `FTPPath` sends `..` as written, and an archive path applies it to the member name and refuses a name that leaves the archive. | A remote tree and an archive hold names, not a current directory, and carry their connection or handle in the object (the model is `MemPath`). Making `==` compare the server too would make a path unequal to its own copy on a second connection, and would break `is_relative_to()`, which decides by `==`. |
|
|
95
95
|
| `full_match()` on a generic path, with a bracket expression | `PurePath.full_match` (3.13+) compiles the whole pattern to one regular expression, so a negated bracket such as `[!x]` can match a separator: `PurePosixPath("a/b").full_match("a[!x]b")` and `PurePosixPath("/").full_match("[!a]")` are `True` | `Pathname.full_match` compares one component at a time and a bracket expression never matches a separator: both are `False` | A component-wise automaton keeps `**` linear in the path and the pattern (a regular expression backtracks exponentially on repeated `**`). A pattern that reads a separator out of a bracket is not a pattern anyone writes on purpose. Everything else, the root included, answers as `pathlib` does. |
|
|
96
96
|
| `copy_into()`, `move_into()`, `replace()` | 3.14: `copy_into(target_dir, **kwargs)` and `move_into(target_dir)` call `copy()`/`move()` and return the new path; `copy()` overwrites an existing file and copies trees. `replace(target)` is `os.replace()` and returns the target | Defined once on `Path` over `copy()`/`move()`, on every backend and version. `copy_into(target_dir, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)` has the keywords and **the defaults of `copy()`**: an existing target needs `overwrite=True` and a directory needs `recursive=True`. `move_into(target_dir, *, overwrite=False)` has those of `move()`. Both return the new path. `replace(target)` is `move(target, overwrite=True)` and returns `target`: a directory is replaced only when empty (`OSError(ENOTEMPTY)` otherwise, nothing removed). On 3.14 `LocalPath` uses these rather than stdlib's `copy_into`/`move_into`; `LocalPath.replace` stays stdlib's `os.replace()` on every version. | One behaviour on every backend, interpreter and `Path` subclass, and one that cannot overwrite or recurse by accident (3.14's default replaces a file silently). A caller that wants 3.14's behaviour passes `overwrite=True, recursive=True`. |
|
|
97
|
-
| `Path.info` and `pathlib.Path(...).copy(target)` onto a `Path` of this library (3.14) | 3.14 `Path.info` is a `PathInfo` kept on the path (its answers are cached for the life of the path object); `pathlib.Path.copy(target)` calls `target._copy_from(source, **kwargs)` on any target with `with_segments()`, so a target without it raises `AttributeError` | `info` is defined on every `Path`, on every version: an object with `exists()`, `is_dir()`, `is_file()` (each with `follow_symlinks=True`) and `is_symlink()`, over at most one following and one non-following `stat()`. A path holds no state, so its answers are cached on the `info` object, not on the path: each `path.info` asks again. `LocalPath` keeps stdlib's `info` on 3.14. `Path._copy_from(source, follow_symlinks=True, preserve_metadata=False)` makes a stdlib path's `copy()` work onto any `Path` of this library (overwrites a file and raises on an existing directory, as 3.14 does; `preserve_metadata` copies the permission bits and the modification time where the class can set them). A class that mixes a stdlib path keeps stdlib's. The private `pathlib` ABCs (`_ReadablePath`, ...) and `__open_rb__`/`__open_wb__` are not implemented. | Code written for 3.14's `PathInfo` and `Path.copy()` accepts these paths. Only the hooks `Path.copy()` reads from its target are carried, since the ABCs are private in 3.14 and have changed between `pathlib-abc` releases. |
|
|
97
|
+
| `Path.info` and `pathlib.Path(...).copy(target)` onto a `Path` of this library (3.14) | 3.14 `Path.info` is a `PathInfo` kept on the path (its answers are cached for the life of the path object); `pathlib.Path.copy(target)` calls `target._copy_from(source, **kwargs)` on any target with `with_segments()`, so a target without it raises `AttributeError` | `info` is defined on every `Path`, on every version: an object with `exists()`, `is_dir()`, `is_file()` (each with `follow_symlinks=True`) and `is_symlink()`, over at most one following and one non-following `stat()`. A path holds no state, so its answers are cached on the `info` object, not on the path: each `path.info` asks again. `LocalPath` keeps stdlib's `info` on 3.14. `Path._copy_from(source, follow_symlinks=True, preserve_metadata=False)` makes a stdlib path's `copy()` work onto any `Path` of this library (overwrites a file and raises on an existing directory, as 3.14 does; a listed child name that is not one safe component raises `ValueError`, as in `copy(recursive=True)`; `preserve_metadata` copies the permission bits and the modification time where the class can set them). A class that mixes a stdlib path keeps stdlib's. The private `pathlib` ABCs (`_ReadablePath`, ...) and `__open_rb__`/`__open_wb__` are not implemented. | Code written for 3.14's `PathInfo` and `Path.copy()` accepts these paths. Only the hooks `Path.copy()` reads from its target are carried, since the ABCs are private in 3.14 and have changed between `pathlib-abc` releases. |
|
|
98
98
|
| `Path.walk()` and a Windows junction | 3.14: `Path.walk()` (`os.walk` with symlinks as files) lists a junction in `filenames` and does not enter it unless `follow_symlinks=True` | A non-following `walk()` lists a junction in `filenames` and does not enter it, on every version and for every class whose `is_junction()` says so (`LocalPath`, and a class for `file:` URIs). `follow_symlinks=True` enters it, as pathlib does. | A junction, like a symlink, is a second name for another tree; a junction that points at its own parent was walked 64 levels deep, until the path got too long. 3.14's behaviour is the safe one and the only one that can be had on 3.9-3.11, where `Path.walk` does not exist. |
|
|
99
99
|
| `LocalPath.symlink_to(target)` with a relative `str` target, on Windows | `pathlib` stores the text as given, `../real.txt` included, and Windows cannot resolve a link whose relative target spells its separators `/` | The path class rewrites the text into Windows separators (`..
|
|
100
100
|
eal.txt`) before the link is made; on POSIX the text is stored as given | A link that cannot be followed is never what a caller wants, and `PathSyncer` recreates links from `as_posix()` text. |
|
|
@@ -171,6 +171,14 @@ because a behavioral decision needed documenting:
|
|
|
171
171
|
(`copy(recursive=True)` on a directory) -- do not invoke `progress`; this
|
|
172
172
|
was a deliberate scope decision for the first cut (generic-stream-only),
|
|
173
173
|
not an oversight. **2026-07-28.**
|
|
174
|
+
- `max_workers=None` on `Path.copy(recursive=True)`, `Path.rm(recursive=True)` and
|
|
175
|
+
`PathSyncer(...)`: no pathlib equivalent. `None` is one thread, as before. An int
|
|
176
|
+
of 2 or more hands the files and links of each directory to a pool of that many
|
|
177
|
+
threads, for a class that sets `_threadsafe = True` (`LocalPath`, `FTPPath`); any
|
|
178
|
+
other class runs serially. Callbacks (`progress`, `on_error`, `ignore_error`, the
|
|
179
|
+
sync `hook`, follow policies) are called one at a time but from pool threads and in
|
|
180
|
+
no fixed order, and the error that ends the operation is the first to happen, not
|
|
181
|
+
the first in listing order.
|
|
174
182
|
- `protocols.checksum.NativeChecksum` (`checksum(algorithm="md5") -> str`) --
|
|
175
183
|
an entirely new, optional protocol with no pathlib equivalent. A `Path`
|
|
176
184
|
subclass may implement it to compute a file digest server-side (e.g.
|
|
@@ -28,10 +28,6 @@ Paths for other places are separate distributions built on this one:
|
|
|
28
28
|
| Distribution | Provides |
|
|
29
29
|
| --- | --- |
|
|
30
30
|
| [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI (`file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:`, `dav:`), and the `uripath` command |
|
|
31
|
-
| [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
|
|
32
|
-
| [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` |
|
|
33
|
-
| [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` |
|
|
34
|
-
| [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport` for `pathapi.http` |
|
|
35
31
|
|
|
36
32
|
## Tour
|
|
37
33
|
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
[project]
|
|
7
7
|
# The distribution and the import package are both `pathapi`.
|
|
8
8
|
name = "pathapi"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.2"
|
|
10
10
|
authors = [{ name = "Jose A" }]
|
|
11
11
|
description = "Generic Path Protocol based pathlib"
|
|
12
12
|
readme = "README.md"
|
|
@@ -43,6 +43,11 @@ docs = ['mkdocs', 'mkdocs-material', 'mkdocstrings[python]']
|
|
|
43
43
|
[project.entry-points."pathapi.http_transports"]
|
|
44
44
|
builtin = "pathapi.http:HTTPTransport"
|
|
45
45
|
|
|
46
|
+
# The async HTTP transports `pathapi.http.AsyncHTTPClient` can send through;
|
|
47
|
+
# `pathapi.http.async_transport_names()` lists them without importing them.
|
|
48
|
+
[project.entry-points."pathapi.http_async_transports"]
|
|
49
|
+
builtin = "pathapi.http:ThreadedTransport"
|
|
50
|
+
|
|
46
51
|
[project.urls]
|
|
47
52
|
Homepage = "https://github.com/jose-pr/pathapi/"
|
|
48
53
|
Documentation = "https://jose-pr.github.io/pathapi/"
|
|
@@ -57,6 +62,8 @@ Issues = "https://github.com/jose-pr/pathapi/issues"
|
|
|
57
62
|
[tool.hatch.build.targets.wheel.force-include]
|
|
58
63
|
"README.md" = "pathapi/README.md"
|
|
59
64
|
# The transport suite (`python -m pathapi.http.testing`) runs from the installed package.
|
|
65
|
+
"tests/transport/test_async_client.py" = "pathapi/http/testing/suite/test_async_client.py"
|
|
66
|
+
"tests/transport/test_async_transport.py" = "pathapi/http/testing/suite/test_async_transport.py"
|
|
60
67
|
"tests/transport/test_client_default.py" = "pathapi/http/testing/suite/test_client_default.py"
|
|
61
68
|
"tests/transport/test_client_errors.py" = "pathapi/http/testing/suite/test_client_errors.py"
|
|
62
69
|
"tests/transport/test_client_peer_text.py" = "pathapi/http/testing/suite/test_client_peer_text.py"
|