pax_global_header00006660000000000000000000000064151466112710014516gustar00rootroot0000000000000052 comment=419c33c28f6ea6d480ae37cf08f0934f54194d86 universal_pathlib-0.3.10/000077500000000000000000000000001514661127100153125ustar00rootroot00000000000000universal_pathlib-0.3.10/.flake8000066400000000000000000000011771514661127100164730ustar00rootroot00000000000000[flake8] ignore= # Whitespace before ':' E203 # Too many leading '#' for block comment E266 # Line break occurred before a binary operator W503 # unindexed parameters in the str.format, see: # https://pypi.org/project/flake8-string-format/ P1 # def statements on the same line with overload E704 max_line_length = 88 max-complexity = 15 select = B,C,E,F,W,T4,B902,T,P show_source = true count = true exclude = .noxfile, .nox, __pycache__, .git, .github, .gitignore, .pytest_cache, upath/tests/pathlib/_test_support.py, upath/tests/pathlib/test_pathlib_3*.py, universal_pathlib-0.3.10/.gitattributes000066400000000000000000000000231514661127100202000ustar00rootroot00000000000000* text=auto eol=lf universal_pathlib-0.3.10/.github/000077500000000000000000000000001514661127100166525ustar00rootroot00000000000000universal_pathlib-0.3.10/.github/dependabot.yml000066400000000000000000000014711514661127100215050ustar00rootroot00000000000000version: 2 updates: - directory: "/" package-ecosystem: "pip" schedule: interval: "weekly" labels: - "maintenance :construction:" groups: # Group all pip dependencies into one PR pip-dependencies: patterns: - "*" # Update via cruft ignore: - dependency-name: "mkdocs*" - dependency-name: "pytest*" - dependency-name: "pylint" - dependency-name: "mypy" - directory: "/" package-ecosystem: "github-actions" schedule: interval: "weekly" labels: - "maintenance :construction:" # Update via cruft ignore: - dependency-name: "actions/checkout" - dependency-name: "actions/setup-python" - dependency-name: "pypa/gh-action-pypi-publish" - dependency-name: "codecov/codecov-action" universal_pathlib-0.3.10/.github/workflows/000077500000000000000000000000001514661127100207075ustar00rootroot00000000000000universal_pathlib-0.3.10/.github/workflows/post-dependabot-update.yml000066400000000000000000000014161514661127100260040ustar00rootroot00000000000000name: Post Dependabot Update on: pull_request: branches: [main] paths: - dev/** jobs: auto-update: if: github.actor == 'dependabot[bot]' runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - uses: hynek/setup-cached-uv@v2 - name: Run tests run: uvx nox --sessions flavours-codegen - name: Commit changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add upath/_flavour_sources.py git commit -m "Auto-update generated flavours" || echo "No changes" git push universal_pathlib-0.3.10/.github/workflows/release.yml000066400000000000000000000011231514661127100230470ustar00rootroot00000000000000name: Release on: release: types: [published] workflow_dispatch: env: FORCE_COLOR: "1" jobs: release: runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - name: Check out the repository uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/setup-cached-uv@v2 - name: Build package run: uvx nox -s build - name: Upload package if: github.event_name == 'release' uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true skip-existing: true universal_pathlib-0.3.10/.github/workflows/tests.yml000066400000000000000000000034571514661127100226050ustar00rootroot00000000000000name: Tests on: push: branches: [main] pull_request: workflow_dispatch: permissions: contents: read env: FORCE_COLOR: "1" concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: tests: timeout-minutes: 10 runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] pyv: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] session: ['tests'] include: - os: ubuntu-latest pyv: '3.9' session: 'tests-minversion' steps: - name: Check out the repository uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/setup-cached-uv@v2 - name: Run tests run: uvx nox --sessions ${{ matrix.session }} --python ${{ matrix.pyv }} -- --cov-report=xml typesafety: runs-on: ubuntu-latest strategy: fail-fast: false matrix: pyv: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - name: Check out the repository uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/setup-cached-uv@v2 - name: Run tests run: uvx nox --sessions type-safety --python ${{ matrix.pyv }} lint: runs-on: ubuntu-latest steps: - name: Check out the repository uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/setup-cached-uv@v2 - name: Lint code and check dependencies run: uvx nox -s lint build: needs: [tests, lint] runs-on: ubuntu-latest steps: - name: Check out the repository uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/setup-cached-uv@v2 - name: Build package run: uvx nox -s build universal_pathlib-0.3.10/.gitignore000066400000000000000000000041521514661127100173040ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ docs/changelog.md # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ venv*/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # setuptools_scm upath/_version.py # vscode workspace settings .vscode/ # mac **/.DS_Store universal_pathlib-0.3.10/.pre-commit-config.yaml000066400000000000000000000031741514661127100216000ustar00rootroot00000000000000default_language_version: python: python3 exclude: ^upath/tests/pathlib/test_pathlib.*\.py|^upath/tests/pathlib/_test_support\.py|^upath/_flavour_sources\.py repos: - repo: https://github.com/psf/black rev: 25.1.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-docstring-first - id: check-executables-have-shebangs - id: check-json - id: check-merge-conflict args: ['--assume-in-merge'] - id: check-toml - id: check-yaml exclude: ^mkdocs\.yml$ - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending args: ['--fix=lf'] - id: sort-simple-yaml - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell args: ['-L', 'fo'] additional_dependencies: ["tomli"] - repo: https://github.com/asottile/pyupgrade rev: v3.19.1 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pycqa/flake8 rev: 7.2.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear==24.1.17 - flake8-comprehensions==3.14.0 - flake8-debugger==4.1.2 - flake8-string-format==0.3.0 - repo: https://github.com/pycqa/bandit rev: 1.8.3 hooks: - id: bandit args: [-c, pyproject.toml] additional_dependencies: ["tomli>=1.1.0"] universal_pathlib-0.3.10/.readthedocs.yaml000066400000000000000000000011511514661127100205370ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version, and other tools you might need build: os: ubuntu-24.04 tools: python: "3.13" jobs: pre_create_environment: - asdf plugin add uv - asdf install uv latest - asdf global uv latest create_environment: - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" install: - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --group docs # Build documentation with Mkdocs mkdocs: configuration: mkdocs.yml universal_pathlib-0.3.10/CHANGELOG.md000066400000000000000000000366541514661127100171410ustar00rootroot00000000000000# universal_pathlib changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.3.10] - 2026-02-22 ### Fixed - upath.extensions: fix ProxyUPath as copy or move target on 3.14+ (#547) ### Changed - upath: updated flavours (#545) - ci: updated development dependencies (#550) ## [0.3.9] - 2026-01-31 ### Fixed - upath.core: raise TypeError if using subclass directly with wrong protocol (#541) - upath.core: (backcompat) TypeError on protocol incompatibility (#540) - upath.extensions: Pydantic serialize ProxyUPath (#538) ### Changed - upath: updated flavours (#512) ## [0.3.8] - 2026-01-11 ### Added - tests: add missing pathlib abc tests (#511) - tests: split base test cases into joinable/readable/writable tests (#507) - docs: describe UPath/s3fs behavior with `is_dir()` (#503) ### Fixed - upath.implementations.cloud: fix S3Path copy to local with name collision of file/dir (#533) - upath.core: fix behaviour of `UPath.parent` and `UPath.parents` (#529) - upath.implementations.github: adjust GitHubPath error reporting (#522) - upath.implementations.cloud: fix error handling on HfPath (#521) - upath.implementations.zip: disable write mode in universal-pathlib (#520) - upath.implementations.tar: fix error handling for write methods (#519) - upath.implementations.http: fix HTTPPath error handling for unsupported methods (#518) - upath.implementations.data: fix DataPath error handling for unsupported methods (#517) - upath.core: fix `touch()` method (#515) - upath.extensions: fix `is_relative_to()` for extensions (#510) - upath.extensions: fix error behavior for `hardlink_to()` backport and `symlink_to()` (#508) - upath: fix `iterdir()` behaviour when raising NotADirectoryError for all UPath subclasses (#506) - tests: xfail on name resolution error in github suite (#523) - tests: fix GitHub tests without a network connection (#509) ### Changed - upath: adjust behavior of `UPath.copy()` and `UPath.copy_into()` with str and Path targets (#530) - upath.core: handover cached fs instances in `with_segments()` (#516) - tests: split test suite according to abcs and cleanup tests (#513) - tests: remove two unused helper functions introduced in #492 (#505) - ci: do not hardcode Python executable name (#504) - ci: updated development dependencies (#501) ## [0.3.7] - 2025-12-03 ### Added - upath.implementations.ftp: added FTPPath support (#485) - tests: added comprehensive warning checks in test suite (#487) - tests: added xfail handling for network connectivity issues (#492) ### Fixed - upath: fixed trailing slash behavior for cloud paths (#488) - upath.core: fixed rename implementation for relative paths (#493) - upath.implementations.memory: fixed MemoryPath root (#495) - upath.implementations.ftp: added support for FTP modification time info format (#485) - upath.implementations.local: fixed rename return type on Python 3.14+ (#493) - upath.extensions: fixed .cwd() behavior for ProxyUPath (#493) - upath.extensions: fixed typing of .cwd() method (#493) - docs: fixed typing example in README (#484) - pypi: fixed maintainer display on PyPI (#486) ### Changed - upath.core: deprecated keyword arguments for UPath.rename() (#496) - ci: updated development dependencies (dvc, huggingface-hub) ## [0.3.6] - 2025-11-13 ### Added - upath: add `UnsupportedOperation` exception for better pathlib compatibility (#474) - upath: backport pathlib 3.10-3.14 method signatures (follow_symlinks, newline, case_sensitive, recurse_symlinks, walk_up, etc.) (#476) - docs: add ProxyUPath usage example (#475) - tests: add comprehensive pathlib backport signature tests (#474, #476, #477) ### Fixed - upath.types: correct `st_birthtime` and `st_birthtime_ns` availability (Windows 3.12+, macOS, FreeBSD only) (#476, #477) - upath: fix `stat()` return type to use `StatResultType` protocol (#476) - upath: fix equality checks for extensions and local paths (#477) - upath.implementations.local: fix `_copy_from` method (#477) - upath.core: remove unneeded lines in `_fs_factory` (#478) ### Changed - upath.core: raise `TypeError` when creating UPath with incompatible protocols (#477) ## [0.3.5] - 2025-11-09 ### Added - upath.implementations.cloud: add `HfPath` for Hugging Face Hub support (#457) - docs: MkDocs documentation on Read The Docs (#468, #469) ### Fixed - upath: fix `relative_to` for simplecache, smb, sftp and extensions (#458) ### Changed - ci: nox and ci updates (#466) - upath: update flavours (#467) ## [0.3.4] - 2025-10-16 ### Added - upath.implementations: add `SimpleCachePath` for simplecache protocol support (#453) - upath: support JSON schema generation for Pydantic integration (#451) ### Changed - upath: move pathlib-abc from `0.5.1` to `>=0.5.1,<0.6.0` (#450) - upath.core: deprecate `_protocol_dispatch=False` parameter (#454) ## [0.3.3] - 2025-10-08 ### Added - upath.implementations: add `ZipPath` for ZIP archive filesystem access (#442) - upath.implementations: add `TarPath` for TAR archive filesystem access (#443) - tests: add chained ZIP and TAR path tests (#440) ### Fixed - upath.core: remove `chain_parser` parameter from type overloads to improve type narrowing (#436) ### Changed - docs: update README with notes about `__fspath__()` behavior ## [0.3.2] - 2025-10-05 ### Added - upath.types: add storage_options submodule with TypedDict classes for all filesystem implementations (#432) - upath.implementations: add storage_options type annotations to all UPath subclass constructors (#432) - upath: add type overloads to narrow UPath type based on protocol parameter (#431) - upath.registry: add overloads to `get_upath_class()` to return correct subclass type based on protocol (#429) - typesafety: add comprehensive tests for storage_options type checking (#432) - typesafety: add tests for protocol-based type narrowing (#429, #431) ### Fixed - upath: fix chained paths `.path` property to return correct normalized paths (#426) - upath.implementations: correct `.path` normalization for cloud and http paths (#426) - upath._protocol: raise error when explicitly requesting empty protocol but another protocol is found (#430) - upath.core: adjust Pydantic v2 schema to support None protocol (#430) - tests: add xfail when hitting GitHub rate limit (#429) ## [0.3.1] - 2025-10-03 ### Added - upath: add `UPath.from_uri()` classmethod (#423) - upath: add `UPath.move_into()` method (#422) - upath: implement `.info` property (#416) - typesafety: add thorough typechecks to UPath interface (#414) ### Fixed - upath: fix type annotations for upath.core, upath.extensions and upath.implementations (#420) - upath: backport types and methods to local implementations (#421) - upath: stricter upath types and remove Compat* protocol (#417) ### Changed - maintenance: update license identifier and restrict ci permissions (#424) ## [0.3.0] - 2025-09-29 ### Fixed - upath: support relative paths (#405) - upath: implement chain functionality (#346) - upath: fix upath suffixes (#407) - upath: update flavours (#350, #351, #400, #403, #411) - upath: fix GH test skipping (#361) - ci: update ubuntu runners (#359) - ci: address skip_existing deprecation (#369) - tests: split protocol mismatch test (#365) - tests: ensure non-local upaths raise with builtin open (#368) - tests: add an xfail test for // behaviour on s3 (#370) - tests: fix xfail call args (#409) - tests: add a os.PathLike test (#410) ### Added - upath: api extensions via `upath.extensions.ProxyUPath` (#372) - upath: add upath.types in preparation for deriving from pathlib-abc (#364) - upath: add optional support for pydantic (#395) - upath: list late registered protocols (#358) - repo: add a security policy (#327) - ci: start running against 3.14 (#363) ### Changed - upath: inherit from `pathlib_abc.ReadablePath` and `pathlib_abc.WritablePath` (#366, #402, #404) - upath: drop Python 3.8 (#360) - upath: remove deprecated accessor support (#362) ## [0.2.6] - 2024-12-13 ### Fixed - upath: add support for 'abfss' protocol in WrappedFileSystemFlavour (#311) - upath: fixed sftp join issue for non-root prefixed paths (#294) - upath: fixed missing typing-extension dependency (#290) - upath: updated flavour sources (#285, #299, #313, #319) - tests: minor fixes for moto and gcs tests without internet connectivity (#312) ### Changed - ci: switch to trusted publishing ### Added - tests: allow configuring smb port via env var (#314) ## [0.2.5] - 2024-09-08 ### Fixed - upath.implementations.cloud: move bucket check to subclasses (#277) - upath: enable local tests on windows and fix is_absolute (#278) - upath: updated flavour sources (#273) ### Added - upath: adds support for python-3.13 (#275) ## [0.2.4] - 2024-09-07 ### Fixed - upath: fix UPath.rename type signature (#258) - upath: prevent SMBPath.rename warnings (#259) - upath: implement UPath.samefile (#261) - upath: fix UPath.touch(exists_ok=False) if file exists (#262) - upath: UPath.joinpath() raise error on protocol mismatch (#264) - tests: silence test warnings (#267) - tests: fix http xpass test (#266) - tests: use newer moto server (#248) - tests: mkdir test on existing gcs bucket (#263) ### Added - upath: add SFTPPath implementation (#265) ### Changed - upath: move setup.cfg to pyproject.toml (#260) - upath: UPath.lstat now returns but raises a warning (#271) - upath: updated flavours to the newest versions (#272) ## [0.2.3] - 2024-08-23 ### Added - upath: add st_birthtime as standard field (#254) - upath: added SMBPath and tests (#219) - ci: added typesafety checks (#212) ### Fixed - upath: fix UPath.is_absolute on <3.12 (#256) - upath: fix UPath.rename for absolute paths (#225) - upath._flavour: fix path parsing due to change in urllib.parse.SplitResult behavior (#236) - upath: fixed typing regressions (#212) - upath: update flavour sources (#224, #237, #252) - docs: fix link to filesystem spec optional dependencies (#232) ## [0.2.2] - 2024-03-04 ### Fixed - upath: fixed comparison with pathlib.Path on py<3.12 (#203) - upath: imports of filesystem classes are now lazy (#200) - upath: open() now passes fsspec options through to fsspec (#204) - upath: fixed regression for args that implement `__fspath__` different from `__str__` (#200) - docs: fixed entrypoint examples for UPath subclass registration (#196) ## [0.2.1] - 2024-02-18 ### Added - upath: added `UPath.joinuri()` (#189) ### Fixed - fixed `UPath` instances not hashable (#188) - fixed missing `packaging` dependency (#187) - fixed pypi package classifiers ## [0.2.0] - 2024-02-13 ### Added - upath: support Python 3.12 (#152) - upath: improved subclass customization options (#173) - upath: support `local` uri scheme (#150) - upath: added `GitHubPath` (#155) - upath: added `DataPath` for data uris (#169) ### Changed - tests: xfail tests if optional dependency is missing (#160) ### Fixed - fixed netloc handling of `memory://netloc/a/b` style uris (#162) - fixed broken mkdir for cloud filesystems (#177) - fixed UPath().stat() now returns a `os.stat_result`-like object (#179) ## [0.1.4] ### Changed - upath: require fsspec>=2022.1.0 (#148). ### Fixed - upath.implementation.local: fixes _kwargs in local sub paths (#158). - upath: fix iterdir trailing slash (#149). - upath: consistent glob behaviour for "**" patterns (#143). ## [0.1.3] ### Fixed - upath: restore compatibility with "fsspec<2022.03.0" in line with setup.cfg (#139). ## [0.1.2] ### Added - upath.registry: provide `available_implementations()` and `register_implementation()` (#134). - upath: add `UPath.storage_options` and `UPath.protocol` (#135). ### Fixed - upath: fix `UPath.as_uri()` (#133). ## [0.1.1] ### Fixed - restore `._kwargs` and `._url` on `PosixUPath` and `WindowsUPath` subclasses (#131). - tests: fixed and refactored core tests (#130). ## [0.1.0] ### Changed - updated past changelog entries. - changed `UPath.__new__` behavior to return `UPath` subclasses for local paths (#125). ### Fixed - improved azure test separation (#123). ### Added - tests to confirm pydantic `BaseSettings` behavior (#127). ## [0.0.24] - 2023-06-19 ### Added - started a changelog to keep track of significant changes (#118). - add support for abfss protocol (#113). - add cpython pathlib tests (#104). - implemented `.rename` (#96). ### Fixed - various webdav test fixes (#103, #107, #109). - fixed issue with `._url` parsing (#102). - improved error messages (#96). - fixed `.rglob()` method (#96). ### Changed - modernized package dev tools (#105). - updated ipynb example notebook (#96). ## [0.0.23] - 2023-03-24 ### Added - Implement `UPath.resolve` with a special redirect-following implementation for `HTTPPath` (#86). ## [0.0.22] - 2023-03-11 ### Fixed - Respect exist_ok in mkdir when creating parent directories (#83). ## [0.0.21] - 2022-09-19 ### Changed - Changed the `UPath` implementation registry to lazily import implementations (#78). - Refactored class methods (#77). ### Fixed - Fixed S3 paths with a `+` (#76). ## [0.0.20] - 2022-08-30 ### Added - Python 3.11 compatibility (#69). ### Fixed - Fix `.parents` (#75). - Fix `.with_*` methods (#73). ### Changed - Use `NotADirectoryError` instead of custom error (#74). ## [0.0.19] - 2022-06-22 ### Added - started a changelog to keep track of significant changes [Unreleased]: https://github.com/fsspec/universal_pathlib/compare/v0.3.10...HEAD [0.3.10]: https://github.com/fsspec/universal_pathlib/compare/v0.3.9...v0.3.10 [0.3.9]: https://github.com/fsspec/universal_pathlib/compare/v0.3.8...v0.3.9 [0.3.8]: https://github.com/fsspec/universal_pathlib/compare/v0.3.7...v0.3.8 [0.3.7]: https://github.com/fsspec/universal_pathlib/compare/v0.3.6...v0.3.7 [0.3.6]: https://github.com/fsspec/universal_pathlib/compare/v0.3.5...v0.3.6 [0.3.5]: https://github.com/fsspec/universal_pathlib/compare/v0.3.4...v0.3.5 [0.3.4]: https://github.com/fsspec/universal_pathlib/compare/v0.3.3...v0.3.4 [0.3.3]: https://github.com/fsspec/universal_pathlib/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/fsspec/universal_pathlib/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/fsspec/universal_pathlib/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/fsspec/universal_pathlib/compare/v0.2.6...v0.3.0 [0.2.6]: https://github.com/fsspec/universal_pathlib/compare/v0.2.5...v0.2.6 [0.2.5]: https://github.com/fsspec/universal_pathlib/compare/v0.2.4...v0.2.5 [0.2.4]: https://github.com/fsspec/universal_pathlib/compare/v0.2.3...v0.2.4 [0.2.3]: https://github.com/fsspec/universal_pathlib/compare/v0.2.2...v0.2.3 [0.2.2]: https://github.com/fsspec/universal_pathlib/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/fsspec/universal_pathlib/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/fsspec/universal_pathlib/compare/v0.1.4...v0.2.0 [0.1.4]: https://github.com/fsspec/universal_pathlib/compare/v0.1.3...v0.1.4 [0.1.3]: https://github.com/fsspec/universal_pathlib/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/fsspec/universal_pathlib/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/fsspec/universal_pathlib/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/fsspec/universal_pathlib/compare/v0.0.24...v0.1.0 [0.0.24]: https://github.com/fsspec/universal_pathlib/compare/v0.0.23...v0.0.24 [0.0.23]: https://github.com/fsspec/universal_pathlib/compare/v0.0.22...v0.0.23 [0.0.22]: https://github.com/fsspec/universal_pathlib/compare/v0.0.21...v0.0.22 [0.0.21]: https://github.com/fsspec/universal_pathlib/compare/v0.0.20...v0.0.21 [0.0.20]: https://github.com/fsspec/universal_pathlib/compare/v0.0.19...v0.0.20 [0.0.19]: https://github.com/fsspec/universal_pathlib/tree/v0.0.19 universal_pathlib-0.3.10/CODE_OF_CONDUCT.rst000066400000000000000000000124261514661127100203260ustar00rootroot00000000000000Contributor Covenant Code of Conduct ==================================== Our Pledge ---------- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. Our Standards ------------- Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting Enforcement Responsibilities ---------------------------- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. Scope ----- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Enforcement ----------- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at andrewfulton9gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. Enforcement Guidelines ---------------------- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 1. Correction ~~~~~~~~~~~~~ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 2. Warning ~~~~~~~~~~ **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 3. Temporary Ban ~~~~~~~~~~~~~~~~ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 4. Permanent Ban ~~~~~~~~~~~~~~~~ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. Attribution ----------- This Code of Conduct is adapted from the `Contributor Covenant `__, version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct/. Community Impact Guidelines were inspired by `Mozilla’s code of conduct enforcement ladder `__. .. _homepage: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. universal_pathlib-0.3.10/CONTRIBUTING.rst000066400000000000000000000051251514661127100177560ustar00rootroot00000000000000Contributor Guide ================= Thank you for your interest in improving this project. This project is open-source under the `MIT license`_ and welcomes contributions in the form of bug reports, feature requests, and pull requests. Here is a list of important resources for contributors: - `Source Code`_ - `Issue Tracker`_ - `Code of Conduct`_ .. _MIT license: https://opensource.org/licenses/MIT .. _Source Code: https://github.com/fsspec/universal_pathlib .. _Issue Tracker: https://github.com/fsspec/universal_pathlib/issues How to report a bug ------------------- Report bugs on the `Issue Tracker`_. When filing an issue, make sure to answer these questions: - Which operating system and Python version are you using? - Which version of this project are you using? - What did you do? - What did you expect to see? - What did you see instead? The best way to get your bug fixed is to provide a test case, and/or steps to reproduce the issue. How to request a feature ------------------------ Request features on the `Issue Tracker`_. How to set up your development environment ------------------------------------------ You need Python 3.8+ and the following tools: - Nox_ Install the package with development requirements: .. code:: console $ pip install nox .. _Nox: https://nox.thea.codes/ How to test the project ----------------------- Run the full test suite: .. code:: console $ nox List the available Nox sessions: .. code:: console $ nox --list-sessions You can also run a specific Nox session. For example, invoke the unit test suite like this: .. code:: console $ nox --session=tests Unit tests are located in the ``tests`` directory, and are written using the pytest_ testing framework. .. _pytest: https://pytest.readthedocs.io/ How to submit changes --------------------- Open a `pull request`_ to submit changes to this project. Your pull request needs to meet the following guidelines for acceptance: - The Nox test suite must pass without errors and warnings. - Include unit tests. This project maintains 100% code coverage. - If your changes add functionality, update the documentation accordingly. Feel free to submit early, though—we can always iterate on this. To run linting and code formatting checks, you can invoke a `lint` session in nox: .. code:: console $ nox -s lint It is recommended to open an issue before starting work on anything. This will allow a chance to talk it over with the owners and validate your approach. .. _pull request: https://github.com/fsspec/universal_pathlib/pulls .. github-only .. _Code of Conduct: CODE_OF_CONDUCT.rst universal_pathlib-0.3.10/LICENSE000066400000000000000000000020571514661127100163230ustar00rootroot00000000000000MIT License Copyright (c) 2022, Andrew Fulton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. universal_pathlib-0.3.10/MANIFEST.in000066400000000000000000000001031514661127100170420ustar00rootroot00000000000000exclude .git* recursive-exclude .git * recursive-exclude .github * universal_pathlib-0.3.10/README.md000066400000000000000000001105471514661127100166010ustar00rootroot00000000000000# Universal Pathlib [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/universal_pathlib)](https://pypi.org/project/universal_pathlib/) [![PyPI - License](https://img.shields.io/pypi/l/universal_pathlib)](https://github.com/fsspec/universal_pathlib/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/universal_pathlib.svg)](https://pypi.org/project/universal_pathlib/) [![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/universal_pathlib?label=conda)](https://anaconda.org/conda-forge/universal_pathlib) [![Docs](https://readthedocs.org/projects/universal-pathlib/badge/?version=latest)](https://universal-pathlib.readthedocs.io/en/latest/?badge=latest) [![Tests](https://github.com/fsspec/universal_pathlib/actions/workflows/tests.yml/badge.svg)](https://github.com/fsspec/universal_pathlib/actions/workflows/tests.yml) [![GitHub issues](https://img.shields.io/github/issues/fsspec/universal_pathlib)](https://github.com/fsspec/universal_pathlib/issues) [![Codestyle black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Changelog](https://img.shields.io/badge/changelog-Keep%20a%20Changelog-%23E05735)](./CHANGELOG.md) Universal Pathlib is a Python library that extends the [`pathlib_abc.JoinablePath`][pathlib_abc] API to provide a [`pathlib.Path`][pathlib]-like interface for a variety of backend filesystems via [`filesystem_spec`][fsspec]. [pathlib_abc]: https://github.com/barneygale/pathlib-abc [pathlib]: https://docs.python.org/3/library/pathlib.html [fsspec]: https://filesystem-spec.readthedocs.io/en/latest/intro.html ## Installation Install the latest version of `universal_pathlib` with pip or conda. Please note that while this will install `fsspec` as a dependency, for some filesystems, you have to install additional packages. For example, to use S3, you need to install `s3fs`, or better depend on `fsspec[s3]`: ### PyPI ```bash python -m pip install universal_pathlib ``` ### conda ```bash conda install -c conda-forge universal_pathlib ``` ### Adding universal_pathlib to your project Below is a `pyproject.toml` based example for adding `universal_pathlib` to your project as a dependency if you want to use it with `s3` and `http` filesystems: ```toml [project] name = "myproject" requires-python = ">=3.9" dependencies = [ "universal_pathlib>=0.3.10", "fsspec[s3,http]", ] ``` See [filesystem_spec/pyproject.toml][fsspec-pyproject-toml] for an overview of the available fsspec extras. [fsspec-pyproject-toml]: https://github.com/fsspec/filesystem_spec/blob/master/pyproject.toml#L26 ## Basic Usage ```pycon # pip install universal_pathlib fsspec[s3] >>> from upath import UPath >>> >>> s3path = UPath("s3://test_bucket") / "example.txt" >>> s3path.name example.txt >>> s3path.stem example >>> s3path.suffix .txt >>> s3path.exists() True >>> s3path.read_text() 'Hello World' ``` For more examples, see the [example notebook here][example-notebook]. [example-notebook]: notebooks/examples.ipynb ### Currently supported filesystems (and protocols) - `file:` and `local:` Local filesystem - `memory:` Ephemeral filesystem in RAM - `az:`, `adl:`, `abfs:` and `abfss:` Azure Storage _(requires `adlfs`)_ - `data:` RFC 2397 style data URLs _(requires `fsspec>=2023.12.2`)_ - `ftp:` FTP filesystem - `github:` GitHub repository filesystem - `hf:` Hugging Face filesystem _(requires `huggingface_hub`)_ - `http:` and `https:` HTTP(S)-based filesystem - `hdfs:` Hadoop distributed filesystem - `gs:` and `gcs:` Google Cloud Storage _(requires `gcsfs`)_ - `s3:` and `s3a:` AWS S3 _(requires `s3fs` to be installed)_ - `sftp:` and `ssh:` SFTP and SSH filesystems _(requires `paramiko`)_ - `smb:` SMB filesystems _(requires `smbprotocol`)_ - `webdav`, `webdav+http:` and `webdav+https:` WebDAV-based filesystem on top of HTTP(S) _(requires `webdav4[fsspec]`)_ It is likely, that other fsspec-compatible filesystems are supported through the default implementation. But because they are not tested in the universal_pathlib test-suite, correct behavior is not guaranteed. If you encounter any issues with a specific filesystem using the default implementation, please open an issue. We are happy to add support for other filesystems via custom UPath implementations. And of course, contributions for new filesystems are welcome! ### Class hierarchy The class hierarchy for `UPath` implementations and their relation to base classes in `pathlib_abc` and the stdlib `pathlib` classes are visualized in the following diagram. Please be aware that the `pathlib_abc.JoinablePath`, `pathlib_abc.ReadablePath`, and `pathlib_abc.WritablePath` classes are currently not actual parent classes of the stdlib pathlib classes. This might occur in later Python releases, but for now, the mental model you should keep is represented by the diagram. ```mermaid flowchart TB subgraph p0[pathlib_abc] X ----> Y X ----> Z end subgraph s0[pathlib] X -.-> A A----> B A--> AP A--> AW Y -.-> B Z -.-> B B--> BP AP----> BP B--> BW AW----> BW end subgraph s1[upath] Y ---> U Z ---> U U --> UP U --> UW BP ---> UP BW ---> UW U --> UL U --> US3 U --> UH U -.-> UO end X(JoinablePath) Y(WritablePath) Z(ReadablePath) A(PurePath) AP(PurePosixPath) AW(PureWindowsPath) B(Path) BP(PosixPath) BW(WindowsPath) U(UPath) UP(PosixUPath) UW(WindowsUPath) UL(FilePath) US3(S3Path) UH(HttpPath) UO(...Path) classDef na fill:#f7f7f7,stroke:#02a822,stroke-width:2px,color:#333 classDef np fill:#f7f7f7,stroke:#2166ac,stroke-width:2px,color:#333 classDef nu fill:#f7f7f7,stroke:#b2182b,stroke-width:2px,color:#333 class X,Y,Z na class A,AP,AW,B,BP,BW,UP,UW np class U,UL,US3,UH,UO nu style UO stroke-dasharray: 3 3 style p0 fill:none,stroke:#0a2,stroke-width:3px,stroke-dasharray:3,color:#0a2 style s0 fill:none,stroke:#07b,stroke-width:3px,stroke-dasharray:3,color:#07b style s1 fill:none,stroke:#d02,stroke-width:3px,stroke-dasharray:3,color:#d02 ``` To be concrete this currently means: ```python # for all supported Python versions: from pathlib import Path from upath import UPath from upath.types import JoinablePath assert isinstance(Path(), JoinablePath) is False assert isinstance(UPath(), JoinablePath) is True ``` When instantiating `UPath` the returned instance type is determined by the path, or better said, the "protocol" that was provided to the constructor. The `UPath` class will return a registered implementation for the protocol, if available. If no specialized implementation can be found but the protocol is available through `fsspec`, it will return a `UPath` instance and provide filesystem access with a default implementation. Please note the default implementation can not guarantee correct behavior for filesystems that are not tested in the test-suite. ### Local paths and url paths If a local path is without protocol is provided `UPath` will return a `PosixUPath` or `WindowsUPath` instance. These two implementations are 100% compatible with the `PosixPath` and `WindowsPath` classes of their specific Python version. They're tested against a large subset of the CPython pathlib test-suite to ensure compatibility. If a local urlpath is provided, i.e. a "file://" or "local://" URI, the returned instance type will be a `FilePath` instance. This class is a subclass of `UPath` that provides file access via `LocalFileSystem` from `fsspec`. You can use it to ensure that all your local file access is done through `fsspec` as well. All local UPath types are `os.PathLike`, but only the `PosixUPath` and `WindowsUPath` are subclasses of `pathlib.Path`. ### UPath public class API The public class interface of `UPath` extends `pathlib.Path` via attributes that simplify interaction with `filesystem_spec`. Think of the `UPath` class in terms of the following code: ```python from pathlib_abc import ReadablePath, WritablePath from typing import Any, Mapping from fsspec import AbstractFileSystem class UPath(ReadablePath, WriteablePath): # the real implementation is more complex, but this is the general idea @property def protocol(self) -> str: """The fsspec protocol for the path.""" @property def storage_options(self) -> Mapping[str, Any]: """The fsspec storage options for the path.""" @property def path(self) -> str: """The path that a fsspec filesystem can use.""" @property def fs(self) -> AbstractFileSystem: """The cached fsspec filesystem instance for the path.""" ``` These attributes are used to provide a public interface to move from the `UPath` instance to more fsspec specific code: ```python from upath import UPath from fsspec import filesystem p = UPath("s3://bucket/file.txt", anon=True) fs = filesystem(p.protocol, **p.storage_options) # equivalent to p.fs with fs.open(p.path) as f: data = f.read() ``` ## Supported Interface Universal Pathlib provides an implementation of the `pathlib.Path` interface across different Python versions. The following table shows the compatibility matrix for stdlib pathlib.Path attributes and methods. Methods supported in UPath should correctly work for all supported Python versions. If not, we consider it a bug. | Method/Attribute | py3.9 | py3.10 | py3.11 | py3.12 | py3.13 | py3.14 | UPath | |---------------------------|-------|--------|--------|--------|--------|--------|-------| | **Pure Paths** | | | | | | | | | _Operators_ | | | | | | | | | `__truediv__` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `__rtruediv__` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | _Access individual Parts_ | | | | | | | | | `parts` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | _Methods and Properties_ | | | | | | | | | `parser` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | `drive` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `root` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `anchor` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `parents` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `parent` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `name` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `suffix` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `suffixes` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `stem` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `as_posix()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_absolute()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_relative_to()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_reserved()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `joinpath()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `full_match()` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | `match()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `relative_to()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `with_name()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `with_stem()` | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | | `with_suffix()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `with_segments()` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | | **Concrete Paths** | | | | | | | | | _Parsing URIs_ | | | | | | | | | `from_uri()` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | `as_uri()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | _Expanding Paths_ | | | | | | | | | `home()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `expanduser()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `cwd()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `absolute()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `resolve()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `readlink()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | _Querying File status_ | | | | | | | | | `stat()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `lstat()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `exists()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_file()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_dir()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_symlink()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_junction()` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | | `is_mount()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_socket()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_fifo()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_block_device()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `is_char_device()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `samefile()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `info` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | _Reading & Writing Files_ | | | | | | | | | `open()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `read_text()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `read_bytes()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `write_text()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `write_bytes()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | _Reading Directories_ | | | | | | | | | `iterdir()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `glob()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `rglob()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `walk()` | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | | _Creating Files & Dirs_ | | | | | | | | | `touch()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `mkdir()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `symlink_to()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | `hardlink_to()` | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | _Copying & Moving_ | | | | | | | | | `copy()` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | `copy_into()` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | `rename()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `replace()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | `move()` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | `move_into()` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | | `unlink()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `rmdir()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | _Permission & Owner_ | | | | | | | | | `owner()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | `group()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | `chmod()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | `lchmod()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ | | **UPath interface** | | | | | | | | | `protocol` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | | `storage_options` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | | `path` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | | `fs` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | | `joinuri()` | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | **Key:** - ✅ = Available/Supported - ❌ = Not available in this version - ⚠️ = Currently raises unsupported error for UPath implementations ## Advanced Usage If you want to create your own UPath implementations, there are multiple ways to customize your subclass behavior. Here are a few things to keep in mind when you create your own UPath implementation: ### UPath's constructor, `upath.registry`, and subclassing When instantiating `UPath(...)` the `UPath.__new__()` method determines the path protocol and returns a registered implementation for the protocol, if available. The registered implementations are mapped in the `upath.registry` module. When a protocol is not registered, `universal_pathlib` checks if the protocol is mapped to an `fsspec` filesystem. If so, it returns an instance of `UPath` and provides filesystem access through the default implementation. The protocol is determined by either looking at the URI scheme of the first argument to the constructor, or by using the `protocol` keyword argument: ```python from upath import UPath from upath.implementations.cloud import S3Path from upath.implementations.memory import MemoryPath p0 = UPath("s3://bucket/file.txt") assert p0.protocol == "s3" assert type(p0) is S3Path assert isinstance(p0, UPath) p1 = UPath("/some/path/file.txt", protocol="memory") assert p1.protocol == "memory" assert type(p1) is MemoryPath assert isinstance(p1, UPath) # the ftp filesystem current has no custom UPath implementation and is not # tested in the universal_pathlib test-suite. Therefore, the default UPath # implementation is returned, and a warning is emitted on instantiation. p2 = UPath("ftp://ftp.ncbi.nih.gov/snp/archive") assert p2.protocol == "ftp" assert type(p2) is UPath ``` This has some implications for custom UPath subclasses. We'll go through the two main cases where you might want to create a custom UPath implementation: #### Case 1: Custom filesystem works with default UPath implementation Let's say you would like to add a new implementation of your "myproto" protocol. You already built a custom AbstractFileSystem implementation for "myproto" which you have registered through `fsspec.registry`. In some cases it is possible that the custom filesystem class already works with `UPath`'s default implementation, and you don't need to necessarily create a custom UPath implementation: ```python import fsspec.registry from fsspec.spec import AbstractFileSystem class MyProtoFileSystem(AbstractFileSystem): protocol = ("myproto",) ... # your custom implementation fsspec.registry.register_implementation("myproto", MyProtoFileSystem) from upath import UPath p = UPath("myproto:///my/proto/path") assert type(p) is UPath assert p.protocol == "myproto" assert isinstance(p.fs, MyProtoFileSystem) ``` #### Case 2: Custom filesystem requires a custom UPath implementation Sometimes the default implementation isn't sufficient and some method(s) have to be overridden to provide correct behavior. In this case, create a custom `UPath` implementation: ```python from upath import UPath class MyProtoPath(UPath): def mkdir(self, mode=0o777, parents=False, exist_ok=False): something = {...: ...} # fixes to make MyProtoFileSystem.mkdir work self.fs.mkdir(self.path, **something) def path(self): path = super().path if path.startswith("/"): return path[1:] # MyProtoFileSystem needs the path without "/" return path ``` If you use your implementation directly via `MyProtoPath("myproto:///a/b")`, you can use this implementation already as is. If you want a call to `UPath(...)` to return your custom implementation when the detected protocol is `"myproto"`, you need to register your implementation. The next section explains your options. Also note: In case you develop a custom `UPath` implementation, please feel free to open an issue to discuss integrating it in `universal_pathlib`. #### Implementation registration dynamically from Python You can register your custom UPath implementation dynamically from Python: ```python # for example: mymodule/submodule.py from upath import UPath from upath.registry import register_implementation class MyProtoPath(UPath): ... # your custom implementation register_implementation("myproto", MyProtoPath) ``` #### Implementation registration on installation via entry points If you distribute your implementation in your own Python package, you can inform `universal_pathlib` about your implementation via the `entry_points` mechanism: ``` # pyproject.toml [project.entry-points."universal_pathlib.implementations"] myproto = "my_module.submodule:MyPath" ``` ``` # setup.cfg [options.entry_points] universal_pathlib.implementations = myproto = my_module.submodule:MyPath ``` Chose the method that fits your use-case best. If you have questions, open a new issue in the `universal_pathlib` repository. We are happy to help you! ### Customization options for UPath subclasses #### Filesystem access methods Once you thoroughly test your custom UPath implementation, it's likely that some methods need to be overridden to provide correct behavior compared to `stdlib`'s `pathlib.Path` class. The most common issue is that for certain edge cases, your implementation is not raising the same exceptions compared to the `pathlib.Path` class. Or that the `UPath.path` property needs some prefix removed or added. ```python class MyProtoPath(UPath): @property def path(self) -> str: if p := self.path.startswith("/"): p = p[1:] return p def mkdir(self, mode=0o777, parents=False, exist_ok=False): if some_edge_case: raise FileExistsError(str(self)) super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok) def is_file(self): return self.fs.isfile(self.path, myproto_option=123) ``` #### Storage option parsing It's possible that you might want to extract additional storage options from the user provided arguments to you constructor. You can provide a custom classmethod for `_parse_storage_options`: ```python import os class MyProtoPath(UPath): @classmethod def _parse_storage_options( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any] ) -> dict[str, Any]: if "SOME_VAR" in os.environ: storage_options["some_var"] = os.environ["SOME_VAR"] storage_options["my_proto_caching"] = True storage_options["extra"] = get_setting_from_path(urlpath) return storage_options ``` #### Fsspec filesystem instantiation To have more control over fsspec filesystem instantiation you can write a custom `_fs_factory` classmethod: ```python class MyProtoPath(UPath): @classmethod def _fs_factory( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any] ) -> AbstractFileSystem: myfs = ... # custom code that creates a AbstractFileSystem instance return myfs ``` #### Init argument parsing In special cases you need to take more control over how the init args are parsed for your custom subclass. You can override `__init__` or the `UPath` classmethod `_transform_init_args`. The latter handles pickling of your custom subclass in a better way in case you modify storage options or the protocol. ```python class MyProtoPath(UPath): @classmethod def _transform_init_args( cls, args: tuple[str | os.PathLike, ...], protocol: str, storage_options: dict[str, Any], ) -> tuple[tuple[str | os.PathLike, ...], str, dict[str, Any]]: # check the cloud, http or webdav implementations for examples ... return args, protocol, storage_options ``` #### Extending UPath's API interface If you want to extend the class API of your `UPath` implementation, it's recommended to subclass `upath.extensions.ProxyUPath`. It's a thin proxy layer around the public methods and attributes of a UPath instance. ```python from upath.extensions import ProxyUPath class ExtraUPath(ProxyUPath): def some_extra_method(self) -> str: return f"hello world {self.name}" e0 = ExtraUPath("s3://bucket/foo.txt") e1 = ExtraUPath("memory://bar/baz.txt") assert e0.some_extra_method() == "hello world foo.txt" assert isinstance(e0, ExtraUPath) assert e1.some_extra_method() == "hello world baz.txt" assert isinstance(e1, ExtraUPath) ``` ## Migration Guide UPath's internal implementation is converging towards a more stable state, with changes in CPython's stdlib `pathlib` having landed in newer Python versions (`3.13`, `3.14`) and the currently private interface for JoinablePaths, ReadablePaths, and WriteablePaths stabilizing. There will likely be other breaking changes down the line, but we'll make the transition as smooth as possible. ### migrating to `v0.3.0` Version `0.3.0` introduced a breaking change to fix a longstanding bug related to `os.PathLike` protocol compliance. This change affects how UPath instances work with standard library functions that expect local filesystem paths. #### Background: PathLike protocol and local filesystem paths In Python, `os.PathLike` objects and `pathlib.Path` subclasses represent local filesystem paths. This is used by the standard library - functions like `os.remove()`, `shutil.copy()`, and similar expect paths that point to the local filesystem. However, UPath implementations like `S3Path` or `MemoryPath` do not represent local filesystem paths and should not be treated as such. Prior to `v0.3.0`, all UPath instances incorrectly implemented `os.PathLike`, which could lead to runtime errors when non-local paths were passed to functions expecting local paths. Starting with `v0.3.0`, only local UPath implementations (`PosixUPath`, `WindowsUPath`, and `FilePath`) implement `os.PathLike`. #### Migration strategy If your code passes UPath instances to functions expecting `os.PathLike` objects, you have several options: **Option 1: Explicitly request a local path** (Recommended) ```python import os from upath import UPath # Explicitly specify the file:// protocol to get a FilePath instance path = UPath(__file__, protocol="file") assert isinstance(path, os.PathLike) # True # Now you can safely use it with os functions os.remove(path) ``` **Option 2: Use UPath's filesystem operations** ```python from upath import UPath # Works for any UPath implementation, not just local paths path = UPath("s3://bucket/file.txt") path.unlink() # UPath's native unlink method ``` **Option 3: Use type checking with upath.types** For code that needs to work with different path types, use the type hints from `upath.types` to properly specify your requirements: ```python import os from upath import UPath from upath.types import ( ReadablePathLike, WritablePathLike, ) def read_only_local_file(path: os.PathLike) -> str: """Read a file on the local filesystem.""" with open(path) as f: return f.read_text() def write_only_local_file(path: os.PathLike) -> None: """Write to a file on the local filesystem.""" with open(path) as f: f.write_text("hello world") def read_any_file(path: ReadablePathLike) -> str: """Read a file on any filesystem.""" return UPath(path).read_text() def write_any_file(path: WritablePathLike) -> None: """Write a file on any filesystem.""" UPath(path).write_text("hello world") ``` #### Example: Incorrect code that would fail The following example shows code that would incorrectly work in `v0.2.x` but properly fail in `v0.3.0`: ```python import os from upath import UPath # This creates a MemoryPath, which is not a local filesystem path path = UPath("memory:///file.txt") # In v0.2.x this would incorrectly accept the path and fail at runtime # In v0.3.0 this correctly fails at type-check time os.remove(path) # TypeError: expected str, bytes or os.PathLike, not MemoryPath ``` #### Extending UPath via `_protocol_dispatch=False` If you previously used `_protocol_dipatch=False` to enable extension of the UPath API, we now recommend to subclass `upath.extensions.ProxyUPath`. See the example in the main docs. ### migrating to `v0.2.0` ### _FSSpecAccessor subclasses with custom filesystem access methods If you implemented a custom accessor subclass, it is now recommended to override the corresponding `UPath` methods in your subclass directly: ```python # OLD: v0.1.x from upath.core import UPath, _FSSpecAccessor class MyAccessor(_FSSpecAccessor): def exists(self, path, **kwargs): # custom code return path.fs.exists(self._format_path(path), **kwargs) def touch(self, path, **kwargs): # custom return path.fs.touch(self._format_path(path), **kwargs) class MyPath(UPath): _default_accessor = MyAccessor # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): def exists(self, *, follow_symlinks=True): kwargs = {} # custom code return self.fs.exists(self.path, **kwargs) def touch(self, mode=0o666, exist_ok=True): kwargs = {} # custom code self.fs.touch(self.path, **kwargs) ``` ### _FSSpecAccessor subclasses with custom `__init__` method If you implemented a custom `__init__` method for your accessor subclass usually the intention is to customize how the fsspec filesystem instance is created. You have two options to recreate this with the new implementation. Chose one or both dependent on the level of control you need. ```python # OLD: v0.1.x import fsspec from upath.core import UPath, _FSSpecAccessor class MyAccessor(_FSSpecAccessor): def __init__(self, parsed_url: SplitResult | None, **kwargs: Any) -> None: # custom code protocol = ... storage_options = ... self._fs = fsspec.filesystem(protocol, storage_options) class MyPath(UPath): _default_accessor = MyAccessor # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): @classmethod def _parse_storage_options( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any] ) -> dict[str, Any]: # custom code to change storage_options storage_options = ... return storage_options @classmethod def _fs_factory( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any] ) -> AbstractFileSystem: # custom code to instantiate fsspec filesystem protocol = ... storage_options = ... # note changes to storage_options here won't # show up in MyPath().storage_options return fsspec.filesystem(protocol, **storage_options) ``` ### Access to `._accessor` The `_accessor` attribute and the `_FSSpecAccessor` class is deprecated. In case you need direct access to the underlying filesystem, just access `UPath().fs`. ```python # OLD: v0.1.x from upath.core import UPath class MyPath(UPath): def mkdir(self, mode=0o777, parents=False, exist_ok=False): self._accessor.mkdir(...) # custom access to the underlying fs... # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): def mkdir(self, mode=0o777, parents=False, exist_ok=False): self.fs.mkdir(...) ``` ### Access to `._path`, `._kwargs`, `._drv`, `._root`, `._parts` If you access one of the listed private attributes directly, move your code over to the following public versions: | _deprecated_ | `v0.2.0+` | |:------------------|:--------------------------| | `UPath()._path` | `UPath().path` | | `UPath()._kwargs` | `UPath().storage_options` | | `UPath()._drv` | `UPath().drive` | | `UPath()._root` | `UPath().root` | | `UPath()._parts` | `UPath().parts` | ### Access to `._url` Be aware that the `._url` attribute will likely be deprecated once `UPath()` has support for uri fragments and uri query parameters through a public api. In case you are interested in contributing this functionality, please open an issue! ### Calling `_from_parts`, `_parse_args`, `_format_parsed_parts` If your code is currently calling any of the three above listed classmethods, it relies on functionality based on the implementation of `pathlib` in Python up to `3.11`. In `universal_pathlib` we vendor code that allows the `UPath()` class to be based on the `3.12` implementation of `pathlib.Path` alone. Usually, usage of those classmethods occurs when copying some code of the internal implementations of methods of the `UPath` `0.1.4` classes. - To reproduce custom `_format_parsed_parts` methods in `v0.2.0`, try overriding `UPath().path` and/or `UPath().with_segments()`. - Custom `_from_parts` and `_parse_args` classmethods can now be implemented via the `_transform_init_args` method or via more functionality in the new flavour class. Please open an issue for discussion in case you have this use case. ### Custom `_URIFlavour` classes The `_URIFlavour` class was removed from `universal_pathlib` and the new flavour class for fsspec filesystem path operations now lives in `upath._flavour`. As of now the internal FSSpecFlavour is experimental. In a future Python version, it's likely that a flavour or flavour-like base class will become public, that allows us to base our internal implementation on. Until then, if you find yourself in a situation where a custom path flavour would solve your problem, please feel free to open an issue for discussion. We're happy to find a maintainable solution. ### Using `.parse_parts()`, `.casefold()`, `.join_parsed_parts()` of `._flavour` These methods of the `._flavour` attribute of `pathlib.Path()` and `UPath()` are specific to `pathlib` of Python versions up to `3.11`. `UPath()` is now based on the `3.12` implementation of `pathlib.Path`. Please refer to the implementations of the `upath._flavour` submodule to see how you could avoid using them. ## Known issues solvable by installing newer upstream dependencies Some issues in `UPath`'s behavior with specific fsspec filesystems are fixed via installation of a newer version of its upstream dependencies. Below you can find a list of known issues and their solutions. We attempt to keep this list updated whenever we encounter more: - currently none :sparkles: ## Contributing Contributions are very welcome. To learn more, see the [Contributor Guide](CONTRIBUTING.rst). ## License Distributed under the terms of the [MIT license](LICENSE), *universal_pathlib* is free and open source software. ## Issues If you encounter any problems, or if you create your own implementations and run into limitations, please [file an issue][issues] with a detailed description. We are always happy to help with any problems you might encounter. [issues]: https://github.com/fsspec/universal_pathlib/issues universal_pathlib-0.3.10/SECURITY.md000066400000000000000000000007111514661127100171020ustar00rootroot00000000000000# Security Policy - Vulnerability Reporting If you believe you have discovered a security issue in universal-pathlib, do not open a public issue. Instead, report it via the repository’s **`Security`** tab using the **`Report a vulnerability`** button. Include clear details and verify whether the vulnerability is in `universal-pathlib` or one of its dependencies. Providing a minimal reproducible example will help resolve the issue more efficiently. universal_pathlib-0.3.10/dev/000077500000000000000000000000001514661127100160705ustar00rootroot00000000000000universal_pathlib-0.3.10/dev/fsspec_inspector/000077500000000000000000000000001514661127100214415ustar00rootroot00000000000000universal_pathlib-0.3.10/dev/fsspec_inspector/generate_flavours.py000066400000000000000000000266011514661127100255330ustar00rootroot00000000000000"""Generates the _flavour_sources.py file""" from __future__ import annotations import inspect import re import sys import warnings from io import StringIO from typing import Any from unittest.mock import Mock from fsspec.registry import available_protocols from fsspec.registry import get_filesystem_class from fsspec.spec import AbstractFileSystem from fsspec.utils import get_package_version_without_import HEADER = '''\ """ upath._flavour_sources Warning ------- Do not modify this file manually! It is generated by `dev/generate_flavours.py` To be able to parse the different filesystem uri schemes, we need the string parsing functionality each of the filesystem implementations. In an attempt to support parsing uris without having to import the specific filesystems, we extract the necessary subset of the AbstractFileSystem classes and generate a new "flavour" class for each of the known filesystems. This will allow us to provide a `PurePath` equivalent `PureUPath` for each protocol in the future without a direct dependency on the underlying filesystem package. """ ''' IMPORTS = """\ from __future__ import annotations import logging import os import re from pathlib import PurePath from pathlib import PureWindowsPath from typing import Any from typing import Literal from typing import cast from urllib.parse import parse_qs from urllib.parse import urlsplit from fsspec.implementations.local import make_path_posix from fsspec.utils import infer_storage_options from fsspec.utils import stringify_path """ INIT_CODE = '''\ __all__ = [ "AbstractFileSystemFlavour", "FileSystemFlavourBase", "flavour_registry", ] logger = logging.getLogger(__name__) flavour_registry: dict[str, type[FileSystemFlavourBase]] = {} class FileSystemFlavourBase: """base class for the fsspec flavours""" protocol: str | tuple[str, ...] root_marker: Literal["/", ""] sep: Literal["/"] @classmethod def _strip_protocol(cls, path): raise NotImplementedError @staticmethod def _get_kwargs_from_urls(path): raise NotImplementedError @classmethod def _parent(cls, path): raise NotImplementedError def __init_subclass__(cls: Any, **kwargs): if isinstance(cls.protocol, str): protocols = (cls.protocol,) else: protocols = tuple(cls.protocol) for protocol in protocols: if protocol in flavour_registry: raise ValueError(f"protocol {protocol!r} already registered") flavour_registry[protocol] = cls ''' BASE_CLASS_NAME_SUFFIX = "Flavour" BASE_CLASS_NAME = f"{AbstractFileSystem.__name__}{BASE_CLASS_NAME_SUFFIX}" SKIP_PROTOCOLS = [ "dir", "blockcache", "cached", "filecache", ] FIX_PROTOCOLS = { "MemFS": ("memfs",), "AsyncLocalFileSystem": (), } FIX_METHODS = { "GCSFileSystem": ["_strip_protocol", "_get_kwargs_from_urls", "_split_path"], "SimpleCacheFileSystem": [], } def _fix_abstract_file_system(x: str) -> str: x = re.sub( "protocol = 'abstract'", "protocol: str | tuple[str, ...] = 'abstract'", x ) x = re.sub("root_marker = ''", "root_marker: Literal['', '/'] = ''", x) x = re.sub("sep = '/'", "sep: Literal['/'] = '/'", x) return x def _fix_azure_blob_file_system(x: str) -> str: x = re.sub( r"if isinstance\(path, list\):", "if isinstance(path, list): # type: ignore[unreachable]", x, ) x = re.sub( r"(return \[.*\])", r"\1 # type: ignore[unreachable]", x, ) return x def _fix_data_file_system(x: str) -> str: return re.sub( "sep = '/'", "sep = '' # type: ignore[assignment]\n " "altsep = ' ' # type: ignore[assignment]", x, ) def _fix_memfs_file_system(x: str) -> str: return re.sub( "_MemFS", "MemoryFileSystemFlavour", x, ) def _fix_memory_file_system(x: str) -> str: return re.sub( "LocalFileSystem", "LocalFileSystemFlavour", x, ) def _fix_oss_file_system(x: str) -> str: x = re.sub( r"path_string: str = stringify_path\(path\)", "path_string = stringify_path(path)", x, ) return x def _fix_xrootd_file_system(x: str) -> str: x = re.sub( r"return client[.]URL\(path\)[.]path_with_params", "x = urlsplit(path); return (x.path + f'?{x.query}' if x.query else '')", x, ) x = re.sub(r"client[.]URL\(u\)", "urlsplit(u)", x) return re.sub( "url.hostid", "url.netloc", x, ) FIX_SOURCE = { "AbstractFileSystem": _fix_abstract_file_system, "AzureBlobFileSystem": _fix_azure_blob_file_system, "DataFileSystem": _fix_data_file_system, "MemFS": _fix_memfs_file_system, "MemoryFileSystem": _fix_memory_file_system, "OSSFileSystem": _fix_oss_file_system, "XRootDFileSystem": _fix_xrootd_file_system, } def before_imports() -> None: """allow to patch the generated state before importing anything""" # patch libarchive sys.modules["libarchive"] = Mock() sys.modules["libarchive.ffi"] = Mock() # patch xrootd sys.modules["XRootD"] = Mock() sys.modules["XRootD.client"] = Mock() sys.modules["XRootD.client.flags"] = Mock() sys.modules["XRootD.client.responses"] = Mock() def get_protos(cls: type, remove: str, add: str) -> tuple[str, ...]: try: return FIX_PROTOCOLS[cls.__name__] except KeyError: pass if isinstance(cls.protocol, str): p = [cls.protocol, add] else: p = [*cls.protocol, add] return tuple([x for x in p if x != remove]) def get_fsspec_filesystems_and_protocol_errors() -> ( tuple[dict[type[AbstractFileSystem], tuple[str, ...]], dict[str, str]] ): before_imports() classes: dict[type[AbstractFileSystem], tuple[str]] = {} errors: dict[str, str] = {} for protocol in available_protocols(): if protocol in SKIP_PROTOCOLS: continue try: cls = get_filesystem_class(protocol) except ImportError as err: errors[protocol] = str(err) else: protos = get_protos(cls, remove="abstract", add=protocol) cprotos = classes.get(cls, []) classes[cls] = tuple(dict.fromkeys([*cprotos, *protos])) return classes, errors def _get_plain_method(cls, name): for c in cls.__mro__: try: return c.__dict__[name] except KeyError: pass else: raise AttributeError(f"{cls.__name__}.{name} not found") def get_subclass_methods(cls: type) -> list[str]: # noqa: C901 try: return FIX_METHODS[cls.__name__] except KeyError: pass errors = [] # storage options so = None base_get_kwargs_from_urls = _get_plain_method( AbstractFileSystem, "_get_kwargs_from_urls" ) try: cls_get_kwargs_from_urls = _get_plain_method(cls, "_get_kwargs_from_urls") except AttributeError: errors.append("missing `_get_kwargs_from_urls()`") else: so = cls_get_kwargs_from_urls is base_get_kwargs_from_urls if not isinstance(cls_get_kwargs_from_urls, staticmethod): warnings.warn( f"{cls.__name__}: {cls_get_kwargs_from_urls!r} not a staticmethod", RuntimeWarning, stacklevel=2, ) # strip protocol sp = None base_strip_protocol = _get_plain_method(AbstractFileSystem, "_strip_protocol") try: cls_strip_protocol = _get_plain_method(cls, "_strip_protocol") except AttributeError: errors.append("missing `_strip_protocol()`") else: if isinstance(cls_strip_protocol, staticmethod): warnings.warn( f"{cls.__name__}: {cls_strip_protocol.__name__!r} is not a classmethod", UserWarning, stacklevel=2, ) sp = False elif isinstance(cls_strip_protocol, classmethod): sp = cls_strip_protocol.__func__ is base_strip_protocol.__func__ else: errors.append( f"{cls.__name__}: {cls_strip_protocol.__name__!r} not a classmethod" ) # _parent pt = None base_parent = _get_plain_method(AbstractFileSystem, "_parent") try: cls_parent = _get_plain_method(cls, "_parent") except AttributeError: errors.append("missing `_parent()`") else: pt = cls_parent is base_parent if errors or sp is None or so is None: raise AttributeError(" AND ".join(errors)) methods = [] if not sp: methods.append("_strip_protocol") if not so: methods.append("_get_kwargs_from_urls") if not pt: methods.append("_parent") return methods def generate_class_source_code( cls: type, methods: list[str], overrides: dict[str, Any], attributes: list[str], cls_suffix: str, base_cls: str | None, ) -> str: s = ["\n"] if base_cls: s += [f"class {cls.__name__}{cls_suffix}({base_cls}):"] else: s += [f"class {cls.__name__}{cls_suffix}:"] mod_ver = get_package_version_without_import(cls.__module__.partition(".")[0]) s.append(f" __orig_class__ = '{cls.__module__}.{cls.__name__}'") s.append(f" __orig_version__ = {mod_ver!r}") for attr, value in overrides.items(): s.append(f" {attr} = {value!r}") for attr in attributes: s.append(f" {attr} = {getattr(cls, attr)!r}") if getattr(cls, "local_file", False): s.append(" local_file = True") s.append("") for method in methods: s.append(inspect.getsource(getattr(cls, method))) try: fix_func = FIX_SOURCE[cls.__name__] except KeyError: return "\n".join(s) else: return "\n".join(fix_func(line) for line in s) def create_source() -> str: buf = StringIO() buf.write(HEADER) classes, errors = get_fsspec_filesystems_and_protocol_errors() srcs = [ generate_class_source_code( AbstractFileSystem, ["_strip_protocol", "_get_kwargs_from_urls", "_parent"], {}, ["protocol", "root_marker", "sep"], cls_suffix=BASE_CLASS_NAME_SUFFIX, base_cls="FileSystemFlavourBase", ) ] for cls in sorted(classes, key=lambda cls: cls.__name__): try: sub_cls_methods = get_subclass_methods(cls) except AttributeError as err: protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol for proto in protos: errors[proto] = str(err) continue sub_cls = generate_class_source_code( cls, sub_cls_methods, {"protocol": classes[cls]}, ["root_marker", "sep"], cls_suffix=BASE_CLASS_NAME_SUFFIX, base_cls=BASE_CLASS_NAME, ) srcs.append(sub_cls) if SKIP_PROTOCOLS: buf.write("#\n# skipping protocols:\n") for protocol in sorted(SKIP_PROTOCOLS): buf.write(f"# - {protocol}\n") if errors: buf.write("# protocol import errors:\n") for protocol, error_msg in sorted(errors.items()): buf.write(f"# - {protocol} ({error_msg})\n") buf.write("#\n") buf.write(IMPORTS) buf.write(INIT_CODE) for cls_src in srcs: buf.write(cls_src) return buf.getvalue().removesuffix("\n") if __name__ == "__main__": print(create_source()) universal_pathlib-0.3.10/dev/requirements.txt000066400000000000000000000006501514661127100213550ustar00rootroot00000000000000fsspec[git,hdfs,dask,http,sftp,smb]==2025.10.0 # these dependencies define their own filesystems adlfs==2025.8.0 boxfs==0.3.0 dropboxdrivefs==1.4.1 gcsfs==2025.10.0 s3fs==2025.10.0 ocifs==1.3.4 webdav4[fsspec]==0.10.0 # gfrivefs @ git+https://github.com/fsspec/gdrivefs@master broken ... morefs[asynclocalfs]==0.2.2 dvc==3.66.1 huggingface_hub==1.4.1 lakefs-spec==0.12.0 ossfs==2025.5.0 fsspec-xrootd==0.5.1 wandbfs==0.0.2 universal_pathlib-0.3.10/docs/000077500000000000000000000000001514661127100162425ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/_plugins/000077500000000000000000000000001514661127100200625ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/_plugins/copy_changelog.py000066400000000000000000000007431514661127100234210ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path THIS_DIR = Path(__file__).parent DOCS_DIR = THIS_DIR.parent PROJECT_ROOT = DOCS_DIR.parent def on_pre_build(**_) -> None: """Add changelog to docs/changelog.md""" cl_now = PROJECT_ROOT.joinpath("CHANGELOG.md").read_text(encoding="utf-8") f_doc = DOCS_DIR.joinpath("changelog.md") if not f_doc.is_file() or f_doc.read_text(encoding="utf-8") != cl_now: f_doc.write_text(cl_now, encoding="utf-8") universal_pathlib-0.3.10/docs/api/000077500000000000000000000000001514661127100170135ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/api/extensions.md000066400000000000000000000033731514661127100215420ustar00rootroot00000000000000# Extensions :puzzle_piece: The extensions module provides a base class for extending UPath functionality while maintaining compatibility with all filesystem implementations. ## ProxyUPath ::: upath.extensions.ProxyUPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true --- ## Usage Example `ProxyUPath` allows you to extend the UPath interface with additional methods while preserving compatibility with all supported filesystem implementations. It acts as a wrapper around any UPath instance. ### Creating a Custom Extension ```python from upath import UPath from upath.extensions import ProxyUPath class MyCustomPath(ProxyUPath): """Custom path with additional functionality""" def custom_method(self) -> str: """Add your custom functionality here""" return f"Custom processing for: {self.path}" def enhanced_read(self) -> str: """Enhanced read with preprocessing""" content = self.read_text() # Add custom processing return content.upper() # Use with any filesystem s3_path = MyCustomPath("s3://bucket/file.txt") local_path = MyCustomPath("/tmp/file.txt") gcs_path = MyCustomPath("gs://bucket/file.txt") # All standard UPath methods work print(s3_path.exists()) print(local_path.parent) # Always a subclass of your class assert isinstance(s3_path, MyCustomPath) assert isinstance(local_path, MyCustomPath) # Plus your custom methods print(s3_path.custom_method()) content = local_path.enhanced_read() ``` --- ## See Also :link: - [UPath](index.md) - Main UPath class documentation - [Implementations](implementations.md) - Built-in UPath subclasses - [Registry](registry.md) - Implementation registry universal_pathlib-0.3.10/docs/api/implementations.md000066400000000000000000000137141514661127100225530ustar00rootroot00000000000000# Implementations :file_folder: Universal Pathlib provides specialized UPath subclasses for different filesystem protocols. Each implementation is optimized for its respective filesystem and may provide additional protocol-specific functionality. ## upath.implementations.cloud ::: upath.implementations.cloud.S3Path options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `s3://`, `s3a://` Amazon S3 compatible object storage implementation. ::: upath.implementations.cloud.GCSPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `gs://`, `gcs://` Google Cloud Storage implementation. ::: upath.implementations.cloud.AzurePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `abfs://`, `abfss://`, `adl://`, `az://` Azure Blob Storage and Azure Data Lake implementation. ::: upath.implementations.cloud.HfPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `hf://` Hugging Face Hub implementation for accessing models, datasets, and spaces. --- ## upath.implementations.local ::: upath.implementations.local.PosixUPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true POSIX-style local filesystem paths (Linux, macOS, Unix). ::: upath.implementations.local.WindowsUPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true Windows-style local filesystem paths. ::: upath.implementations.local.FilePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `file://`, `local://` File URI implementation for local filesystem. --- ## upath.implementations.http ::: upath.implementations.http.HTTPPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `http://`, `https://` HTTP/HTTPS read-only filesystem implementation. --- ## upath.implementations.sftp ::: upath.implementations.sftp.SFTPPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `sftp://`, `ssh://` SFTP (SSH File Transfer Protocol) implementation. --- ## upath.implementations.smb ::: upath.implementations.smb.SMBPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `smb://` SMB/CIFS network filesystem implementation. --- ## upath.implementations.webdav ::: upath.implementations.webdav.WebdavPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocols:** `webdav://`, `webdav+http://`, `webdav+https://` WebDAV protocol implementation. --- ## upath.implementations.hdfs ::: upath.implementations.hdfs.HDFSPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `hdfs://` Hadoop Distributed File System implementation. --- ## upath.implementations.github ::: upath.implementations.github.GitHubPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `github://` GitHub repository file access implementation. --- ## upath.implementations.zip ::: upath.implementations.zip.ZipPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `zip://` ZIP archive filesystem implementation. --- ## upath.implementations.tar ::: upath.implementations.tar.TarPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `tar://` TAR archive filesystem implementation. --- ## upath.implementations.memory ::: upath.implementations.memory.MemoryPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `memory://` In-memory filesystem implementation for testing and temporary storage. --- ## upath.implementations.data ::: upath.implementations.data.DataPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `data://` Data URL scheme implementation for embedded data. --- ## upath.implementations.ftp ::: upath.implementations.ftp.FTPPath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `ftp://` FTP (File Transfer Protocol) implementation. --- ## upath.implementations.cached ::: upath.implementations.cached.SimpleCachePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: [] show_bases: true **Protocol:** `simplecache://` Local caching wrapper for remote filesystems. --- ## See Also :link: - [UPath](index.md) - Main UPath class documentation - [Registry](registry.md) - Implementation registry - [Extensions](extensions.md) - Extending UPath functionality universal_pathlib-0.3.10/docs/api/index.md000066400000000000000000000041651514661127100204520ustar00rootroot00000000000000 # UPath ![upath](../assets/logo-128x128.svg){: #upath-logo } The `UPath` class is your default entry point for interacting with fsspec filesystems. When instantiating UPath, a specific `UPath` subclass will be returned, dependent on the detected or provided `protocol`. Here we document all methods and properties available on UPath instances. !!! info "Compatibility" All methods documented here work consistently across all supported Python versions, even if they were introduced in later Python versions. We consider it a bug if they don't :bug: so please report and issue if you run into inconsistencies. ```python from upath import UPath ``` ::: upath.core.UPath options: heading_level: 2 merge_init_into_class: false inherited_members: true members: - __init__ - protocol - storage_options - fs - path - parts - name - stem - drive - root - anchor - suffix - suffixes - parent - parents - joinpath - joinuri - with_name - with_stem - with_suffix - with_segments - relative_to - is_relative_to - match - full_match - as_posix - as_uri - open - read_text - read_bytes - write_text - write_bytes - iterdir - glob - rglob - walk - mkdir - rmdir - touch - unlink - rename - replace - copy - move - copy_into - move_into - exists - is_file - is_dir - is_symlink - is_absolute - stat - info - absolute - resolve - expanduser - cwd - home --- ## See Also :link: - [Registry](registry.md) - The upath implementation registry - [Implementations](implementations.md) - UPath subclasses - [Extensions](extensions.md) - Extending UPath functionality - [Types](types.md) - Type hints and protocols universal_pathlib-0.3.10/docs/api/registry.md000066400000000000000000000016131514661127100212060ustar00rootroot00000000000000# Registry :file_cabinet: The UPath registry system manages filesystem-specific path implementations. It allows you to register custom UPath subclasses for different protocols and retrieve the appropriate implementation for a given protocol. ## Functions ::: upath.registry.get_upath_class options: heading_level: 3 show_root_heading: true show_root_full_path: false ::: upath.registry.register_implementation options: heading_level: 3 show_root_heading: true show_root_full_path: false ::: upath.registry.available_implementations options: heading_level: 3 show_root_heading: true show_root_full_path: false --- ## See Also :link: - [UPath](index.md) - Main UPath class documentation - [Implementations](implementations.md) - Built-in UPath subclasses - [Extensions](extensions.md) - Extending UPath functionality universal_pathlib-0.3.10/docs/api/types.md000066400000000000000000000073531514661127100205110ustar00rootroot00000000000000# Types :label: The types module provides type hints, protocols, and type aliases for working with UPath and filesystem operations. This includes abstract base classes, type aliases for path-like objects, and typed dictionaries for filesystem-specific storage options. ## pathlib-abc base classes These abstract base classes and protocols are re-exported from [pathlib-abc](https://github.com/barneygale/pathlib-abc) They define the core path interfaces that stdlib pathlib and UPath implementations conform to. ::: upath.types.JoinablePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true ::: upath.types.ReadablePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true ::: upath.types.WritablePath options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true ::: upath.types.PathInfo options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true ::: upath.types.PathParser options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true --- ## UPath specific protocols ::: upath.types.UPathParser options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false show_bases: true --- ## Type Aliases Convenient type aliases for path-like objects used throughout UPath. ::: upath.types.JoinablePathLike options: heading_level: 3 show_root_heading: true show_root_full_path: false Union of types that can be joined as path segments. ::: upath.types.ReadablePathLike options: heading_level: 3 show_root_heading: true show_root_full_path: false Union of types that can be read from. ::: upath.types.WritablePathLike options: heading_level: 3 show_root_heading: true show_root_full_path: false Union of types that can be written to. ::: upath.types.SupportsPathLike options: heading_level: 3 show_root_heading: true show_root_full_path: false Union of objects that support `__fspath__()` or `__vfspath__()` protocols. ::: upath.types.StatResultType options: heading_level: 3 show_root_heading: true show_root_full_path: false members: false Protocol for `os.stat_result`-like objects. --- ## Storage Options Typed dictionaries providing type hints for filesystem-specific configuration options. These help ensure correct parameter names and types when configuring different filesystems. ::: upath.types.storage_options options: heading_level: 3 show_root_heading: true show_root_full_path: false show_bases: false members: - SimpleCacheStorageOptions - GCSStorageOptions - S3StorageOptions - AzureStorageOptions - DataStorageOptions - FTPStorageOptions - GitHubStorageOptions - HDFSStorageOptions - HTTPStorageOptions - FileStorageOptions - MemoryStorageOptions - SFTPStorageOptions - SMBStorageOptions - WebdavStorageOptions - ZipStorageOptions - TarStorageOptions --- ## See Also :link: - [UPath](index.md) - Main UPath class documentation - [Implementations](implementations.md) - Built-in UPath subclasses - [Extensions](extensions.md) - Extending UPath functionality - [Registry](registry.md) - Implementation registry universal_pathlib-0.3.10/docs/assets/000077500000000000000000000000001514661127100175445ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/assets/favicon.png000066400000000000000000000026311514661127100217010ustar00rootroot00000000000000PNG  IHDR szz pHYs'tEXtSoftwarewww.inkscape.org<&IDATXWkle=Gi+ ZlP[")&$)%&k[C? ؆`bEtnI7csg!9ϐZ@ʵ 8 $FGFhN(|>h`R<xIbn3D#`!:soOuV86l02&ɹB׫u[N( HHΧ32vAK@֞|^n]3lVmsn_^yE :c,z";,wg7-Ka'Ȁޒ =)TuM=Oh>}Pd Kx6>iɁ9mc+/a%R TȒe5z"nnsh4DSz 4HDHME@3% ,LNU'N&K*yR;삺3d\C `FQ@r"Ta-"J 0VM6'=qcښ[RX~ǭ󉙊ePR"`mlbA=?*9@$ݶpz0`ؾ@Yʒ|eyrBAD:7%lED0!<*PKr=kK& 4Ks?4uҦ:C,ɂ &ͦ93n8^]{|JC*O 8DЎ[[V6g<4MN@Q& 폝%,:r:ι7.`0Dj3a3h%Se1!i69%25E[(6 t%\;C򪺴?4k d0v*^@'jTJ@TD1蚕 zw˦uex0 _>SA:dR"ZOPIENDB`universal_pathlib-0.3.10/docs/assets/logo-128x128-white.svg000066400000000000000000000107511514661127100233020ustar00rootroot00000000000000 universal_pathlib-0.3.10/docs/assets/logo-128x128.svg000066400000000000000000000203141514661127100221600ustar00rootroot00000000000000 universal_pathlib-0.3.10/docs/assets/logo-text.svg000066400000000000000000000434371514661127100222220ustar00rootroot00000000000000 universal_pathlib-0.3.10/docs/concepts/000077500000000000000000000000001514661127100200605ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/concepts/fsspec.md000066400000000000000000000142421514661127100216700ustar00rootroot00000000000000# Filesystem Spec :file_folder: [fsspec](https://filesystem-spec.readthedocs.io/) is a Python library that provides a unified, pythonic interface for working with different storage backends. It abstracts away the differences between various storage systems, allowing you to interact with local files, cloud storage, remote systems, and specialty filesystems using a consistent API. ## What is fsspec? fsspec is both a **specification** and a **collection of implementations** for pythonic filesystems. The project defines a standard interface that filesystem implementations should follow, then provides concrete implementations for dozens of different storage backends. The core idea is simple: whether you're working with files on your local disk, objects in an S3 bucket, blobs in Azure storage, or data over HTTP, the API to interact with them should be the same. ### Core Functionality fsspec provides filesystem objects with methods for common operations. All filesystem implementations inherit from `fsspec.spec.AbstractFileSystem`, which defines the standard interface that all filesystems must implement: ```python import fsspec # Create a filesystem instance # Returns an AbstractFileSystem subclass for the specified protocol fs = fsspec.filesystem('s3', anon=True) # List files files = fs.ls('my-bucket/data/') # Check if file exists exists = fs.exists('my-bucket/data/file.txt') # Get file info info = fs.info('my-bucket/data/file.txt') # Read file with fs.open('my-bucket/data/file.txt', 'r') as f: content = f.read() # Write file with fs.open('my-bucket/output.txt', 'w') as f: f.write('Hello, World!') # Copy files fs.cp('my-bucket/source.txt', 'my-bucket/dest.txt') # Delete files fs.rm('my-bucket/file.txt') ``` ### Protocols fsspec identifies filesystem types via **protocols**. Each protocol corresponds to a specific filesystem implementation: - `file://` - Local filesystem - `memory://` - In-memory filesystem (temporary, non-persistent) - `s3://` or `s3a://` - Amazon S3 - `gs://` or `gcs://` - Google Cloud Storage - `az://` or `abfs://` - Azure Blob Storage - `adl://` - Azure Data Lake Gen1 - `abfss://` - Azure Data Lake Gen2 (secure) - `http://` or `https://` - HTTP(S) access - `ftp://` - FTP - `sftp://` or `ssh://` - SFTP over SSH - `smb://` - Samba/Windows file shares - `webdav://` or `webdav+http://` - WebDAV - `hdfs://` - Hadoop Distributed File System - `hf://` - Hugging Face Hub - `github://` - GitHub repositories - `zip://` - ZIP archives - `tar://` - TAR archives - `gzip://` - GZIP compressed files - `cached://` - Caching layer over other filesystems And many more. See the [fsspec documentation](https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations) for the complete list. ### Storage Options Each filesystem implementation accepts different configuration parameters called **storage options**. These control authentication, connection settings, caching behavior, and more. They are usually provided as keyword parameters to the specific filesystem class on instantiation. Common storage option patterns: ```python import fsspec # Authentication credentials fs = fsspec.filesystem('s3', key='...', secret='...') # Anonymous/public access fs = fsspec.filesystem('s3', anon=True) # Tokens and service accounts fs = fsspec.filesystem('gs', token='path/to/creds.json') # Connection settings fs = fsspec.filesystem('sftp', host='...', port=22, username='...') # Behavioral options fs = fsspec.filesystem('s3', use_ssl=True, default_block_size=5*2**20) ``` Refer to the [fsspec documentation](https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations) for details on what each filesystem supports. ### URI-Based Access: urlpaths fsspec supports opening files directly using URIs. Usually a resource is clearly defined by its 'protocol', 'storage options', and 'path'. The protocol and path can usually be combined to a urlpath string: ```python import fsspec # resource protocol = "s3" storage_options = {"anon": True} path = "bucket/file.txt" # Create filesystem and open path fs = fsspec.filesystem("s3", anon=True) with fs.open("bucket/file.txt", "r") as f: content = f.read() # Or open a file via its urlpath with storage_options with fsspec.open('s3://bucket/file.txt', 'r', anon=True) as f: content = f.read() ``` ### Chained Filesystems fsspec supports composing filesystems together using the `::` separator. This allows one filesystem to be used as the target filesystem for another: ```python import fsspec # Access a file inside a ZIP archive on S3 with fsspec.open('zip://data.csv::s3://bucket/archive.zip', 'r', anon=True) as f: content = f.read() # Read a compressed file with fsspec.open('tar://file.txt::s3://bucket/archive.tar', 'r', anon=True) as f: content = f.read() ``` ### Caching fsspec includes powerful caching capabilities to improve performance when accessing remote files: ```python import fsspec # Simple caching fs = fsspec.filesystem( 's3', anon=True, use_listings_cache=True, listings_expiry_time=600 # Cache for 10 minutes ) # File-level caching cached_fs = fsspec.filesystem( 'filecache', target_protocol='s3', target_options={'anon': True}, cache_storage='/tmp/fsspec-cache' ) ``` ## When to use fsspec directly You typically use fsspec directly when you: - Need filesystem-level operations (`ls`, `cp`, `rm`, `find`) - Want to work with file-like objects without path abstractions - Need low-level control over filesystem behavior - Are integrating with data libraries that accept fsspec URLs - Want to implement custom filesystem wrappers - Want to avoid the overhead of UPath instance creation ## Learn More For comprehensive information about fsspec: - **Official documentation**: [fsspec.readthedocs.io](https://filesystem-spec.readthedocs.io/) - **API reference**: [Built-in filesystem implementations](https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations) - **GitHub repository**: [fsspec/filesystem_spec](https://github.com/fsspec/filesystem_spec) - **Usage guides**: [Examples and tutorials](https://filesystem-spec.readthedocs.io/en/latest/usage.html) For using fsspec with a pathlib-style interface, see [upath.md](upath.md). universal_pathlib-0.3.10/docs/concepts/index.md000066400000000000000000000021201514661127100215040ustar00rootroot00000000000000# Overview :map: Universal Pathlib brings together fsspec and pathlib to provide a unified, pythonic interface for working with files across different storage systems. Understanding how these components work together will help you make the most of universal-pathlib. - **[Filesystem Spec](fsspec.md)** provides the foundation—a specification and collection of filesystem implementations that offer consistent access to local storage, cloud services, and remote systems. - **[Pathlib](pathlib.md)** defines the familiar object-oriented API from Python's standard library for working with filesystem paths. - **[Universal Pathlib](upath.md)** ties them together, implementing the [pathlib-abc](https://github.com/barneygale/pathlib-abc) interface on top of fsspec filesystems to give you a Path-like experience everywhere. Start with [fsspec filesystems](fsspec.md) to understand the available storage backends, then explore [stdlib pathlib](pathlib.md) to learn about the path interface, and finally see [upath](upath.md) to discover how universal-pathlib combines them into a powerful, unified API. universal_pathlib-0.3.10/docs/concepts/pathlib.md000066400000000000000000000133571514661127100220360ustar00rootroot00000000000000# Pathlib :snake: [pathlib](https://docs.python.org/3/library/pathlib.html) is a Python standard library module that provides an object-oriented interface for working with filesystem paths. It's the modern, pythonic way to handle file paths and filesystem operations, replacing the older string-based `os.path` approach. ## What is pathlib? Introduced in Python 3.4, pathlib represents filesystem paths as objects rather than strings. ### Path Objects In pathlib, paths are instances of `Path` (or platform-specific subclasses) that represent local filesystem paths: ```python from pathlib import Path # Create path objects p = Path("/home/user/documents") p = Path("relative/path/to/file.txt") p = Path.home() # User's home directory p = Path.cwd() # Current working directory ``` ### Pure vs. Concrete Paths pathlib distinguishes between two types of paths: **Pure Paths** (`PurePath`, `PurePosixPath`, `PureWindowsPath`): - Only manipulate path strings - Don't access the filesystem - Work on any platform regardless of OS - Useful for path manipulation without I/O ```python from pathlib import PurePath, PurePosixPath, PureWindowsPath # Pure path - string manipulation only pure = PurePath("/home/user/file.txt") parent = pure.parent # Works name = pure.name # Works # exists = pure.exists() # AttributeError - no filesystem access # Platform-specific pure paths posix = PurePosixPath("/home/user/file.txt") # Always uses / windows = PureWindowsPath("C:\\Users\\file.txt") # Always uses \ ``` **Concrete Paths** (`Path`, `PosixPath`, `WindowsPath`): - Inherit from pure paths - Actually access the filesystem - Support operations like `.exists()`, `.stat()`, `.read_text()` - Platform-specific: `PosixPath` on Unix, `WindowsPath` on Windows ```python from pathlib import Path # Concrete path - filesystem operations p = Path("/home/user/file.txt") exists = p.exists() # Checks filesystem content = p.read_text() # Reads file size = p.stat().st_size # Gets file size ``` ## When to use pathlib Use pathlib when you: - Work with local filesystem paths in Python - Need cross-platform path handling - Want object-oriented path manipulation ## What is pathlib-abc? [pathlib-abc](https://github.com/barneygale/pathlib-abc) is a Python library that defines abstract base classes (ABCs) for path-like objects. It provides a formal specification for the pathlib interface that can be implemented by different path types, not just local filesystem paths. ### Abstract Base Classes for Paths pathlib-abc extracts the core concepts from Python's pathlib module into abstract base classes. This allows library authors and framework developers to: 1. **Define path-like interfaces** that work across different storage backends 2. **Type hint** functions that accept any path-like object 3. **Implement custom path classes** that follow pathlib conventions 4. **Ensure compatibility** between different path implementations !!! info "Relationship to Python's pathlib" Currently (as of Python 3.14), the standard library `pathlib.Path` does **not** inherit from public pathlib-abc classes. However, there is ongoing work to incorporate these ABCs into future Python releases. The library defines three main abstract base classes that represent different levels of path functionality: ### JoinablePath `JoinablePath` is the most basic path abstraction. It represents paths that can be constructed, manipulated, and joined together, but cannot necessarily access any actual filesystem. **Key capabilities:** - Path construction and manipulation - String operations on paths - Path component access (name, stem, suffix, parent, etc.) - Path joining with the `/` operator - Pattern matching Think of `JoinablePath` as equivalent to pathlib's `PurePath` - it only manipulates path strings. ### ReadablePath `ReadablePath` extends `JoinablePath` to add read-only filesystem operations. It represents paths where you can read data but not modify the filesystem. **Adds capabilities for:** - Reading file contents (`.read_text()`, `.read_bytes()`) - Opening files for reading - Checking file existence and type (`.exists()`, `.is_file()`, `.is_dir()`) - Listing directory contents (`.iterdir()`) - Globbing and pattern matching (`.glob()`, `.rglob()`) - Walking directory trees (`.walk()`) - Reading symlinks (`.readlink()`) - Accessing file metadata (`.info` property) ### WritablePath `WritablePath` extends `JoinablePath` (not `ReadablePath`) to add write operations. It represents paths where you can create, modify, and delete filesystem objects. **Adds capabilities for:** - Writing file contents (`.write_text()`, `.write_bytes()`) - Opening files for writing - Creating directories (`.mkdir()`) - Creating symlinks (`.symlink_to()`) !!! note "WritablePath Does Not Inherit from ReadablePath" `WritablePath` does NOT inherit from `ReadablePath`. A path that is writable is not automatically readable. In practice, most filesystem paths are both readable and writable (like `UPath` which inherits from both), but the separation allows for specialized use cases like write-only destinations or read-only sources. ## Learn More For comprehensive information about pathlib: - **Official documentation**: [Python pathlib documentation](https://docs.python.org/3/library/pathlib.html) - **PEP 428**: [The pathlib module – object-oriented filesystem paths](https://www.python.org/dev/peps/pep-0428/) - **Comparison with os.path**: [Correspondence to tools in the os module](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module) For comprehensive information about pathlib-abc: - **GitHub repository**: [barneygale/pathlib-abc](https://github.com/barneygale/pathlib-abc) For using pathlib-style paths with remote and cloud filesystems, see [upath.md](upath.md). universal_pathlib-0.3.10/docs/concepts/upath.md000066400000000000000000000171401514661127100215260ustar00rootroot00000000000000 # Universal Pathlib ![upath](../assets/logo-128x128.svg){: #upath-logo } **universal-pathlib** (imported as `upath`) bridges Python's [pathlib](https://docs.python.org/3/library/pathlib.html) API with [fsspec](https://filesystem-spec.readthedocs.io/)'s filesystem implementations. It provides a familiar, pathlib-style interface for working with files across local storage, cloud services, and remote systems. ## The Best of Both Worlds universal-pathlib combines: - **fsspec's filesystem support**: Access to S3, GCS, Azure, HDFS, HTTP, SFTP, and dozens more backends - **pathlib's elegant API**: Object-oriented paths, `/` operator, `.exists()`, `.read_text()`, etc. This means you can write code using the pathlib syntax you already know, and it works seamlessly across any storage system that fsspec supports. ## How UPath and Path Relate via pathlib-abc `UPath` and `pathlib.Path` are related through the abstract base classes defined in [pathlib-abc](https://github.com/barneygale/pathlib-abc). While they share a common API design, they serve different purposes and have distinct inheritance hierarchies. ### The Class Hierarchy The following diagram shows how `UPath` implementations relate to `pathlib` classes through the `pathlib_abc` abstract base classes: ```mermaid flowchart TB subgraph p0[pathlib_abc] X ----> Y X ----> Z end subgraph s0[pathlib] X -.-> A A----> B A--> AP A--> AW Y -.-> B Z -.-> B B--> BP AP----> BP B--> BW AW----> BW end subgraph s1[upath] Y ---> U Z ---> U U --> UP U --> UW BP ---> UP BW ---> UW U --> UL U --> US3 U --> UH U -.-> UO end X(JoinablePath) Y(WritablePath) Z(ReadablePath) A(PurePath) AP(PurePosixPath) AW(PureWindowsPath) B(Path) BP(PosixPath) BW(WindowsPath) U(UPath) UP(PosixUPath) UW(WindowsUPath) UL(FilePath) US3(S3Path) UH(HttpPath) UO(...Path) classDef na fill:#f7f7f7,stroke:#02a822,stroke-width:2px,color:#333 classDef np fill:#f7f7f7,stroke:#2166ac,stroke-width:2px,color:#333 classDef nu fill:#f7f7f7,stroke:#b2182b,stroke-width:2px,color:#333 class X,Y,Z na class A,AP,AW,B,BP,BW,UP,UW np class U,UL,US3,UH,UO nu style UO stroke-dasharray: 3 3 style p0 fill:none,stroke:#0a2,stroke-width:3px,stroke-dasharray:3,color:#0a2 style s0 fill:none,stroke:#07b,stroke-width:3px,stroke-dasharray:3,color:#07b style s1 fill:none,stroke:#d02,stroke-width:3px,stroke-dasharray:3,color:#d02 ``` **Legend:** - **Green (pathlib_abc)**: Abstract base classes defining the path interface - **Blue (pathlib)**: Standard library path classes for local filesystems - **Red (upath)**: Universal pathlib classes for all filesystems - Solid lines: Direct inheritance - Dotted lines: Conceptual relationship (not actual inheritance yet) ### Understanding the Relationships **pathlib-abc Layer (Green):** - `JoinablePath` - Basic path manipulation without filesystem access - `ReadablePath` - Adds read-only filesystem operations - `WritablePath` - Adds write filesystem operations **pathlib Layer (Blue):** - `PurePath` - Pure path manipulation (similar to `JoinablePath` conceptually) - `Path` - Concrete local filesystem paths (conceptually similar to `ReadablePath` + `WritablePath`) - Platform-specific: `PosixPath`, `WindowsPath`, etc. **universal-pathlib Layer (Red):** - `UPath` - Universal path for any filesystem backend - Local implementations: `PosixUPath`, `WindowsUPath`, `FilePath` - Remote implementations: `S3Path`, `HttpPath`, and others ### Key Differences **Current State (Python 3.9-3.13):** ```python from pathlib import Path from upath import UPath from upath.types import JoinablePath, ReadablePath, WritablePath # UPath explicitly implements pathlib-abc path = UPath("s3://bucket/file.txt") assert isinstance(path, JoinablePath) # True assert isinstance(path, ReadablePath) # True assert isinstance(path, WritablePath) # True # pathlib.Path does NOT (yet) inherit from pathlib-abc local = Path("/home/user/file.txt") assert isinstance(local, JoinablePath) # False assert isinstance(local, ReadablePath) # False assert isinstance(local, WritablePath) # False ``` **Important Note:** The dotted lines in the diagram represent a conceptual relationship. While `pathlib.Path` doesn't currently inherit from `pathlib_abc` classes, it implements a compatible API. Future Python versions may formalize this relationship. ### Local Path Compatibility For local filesystem paths, `UPath` provides implementations that are 100% compatible with stdlib `pathlib`: ```python from pathlib import Path, PosixPath, WindowsPath from upath import UPath # Without protocol -> returns platform-specific UPath local = UPath("/home/user/file.txt") assert isinstance(local, UPath) # True assert isinstance(local, PosixPath) # True (on Unix systems) assert isinstance(local, Path) # True # With file:// protocol -> returns FilePath (fsspec-based) file_path = UPath("file:///home/user/file.txt") assert isinstance(file_path, UPath) # True assert not isinstance(file_path, Path) # False (uses fsspec instead) ``` **PosixUPath and WindowsUPath:** - Subclass both `UPath` and `pathlib.Path` - 100% compatible with stdlib pathlib for local paths - Tested against CPython's pathlib test suite - Implement `os.PathLike` protocol **FilePath:** - Subclass of `UPath` only - Uses fsspec's `LocalFileSystem` for file access - Useful for consistent fsspec-based access across all backends - Implements `os.PathLike` protocol ### Remote and Cloud Paths For remote filesystems, `UPath` implementations provide the pathlib API backed by fsspec: ```python from upath import UPath # S3Path s3 = UPath("s3://bucket/file.txt") assert isinstance(s3, UPath) assert not isinstance(s3, Path) # Not a local path # HttpPath http = UPath("https://example.com/data.json") assert isinstance(http, UPath) assert not isinstance(http, Path) # Not a local path ``` ### Why This Design? This architecture provides several benefits: 1. **Unified API**: Same pathlib interface works across all backends 2. **Type Safety**: pathlib-abc provides formal type hints for path operations 3. **Local Compatibility**: `PosixUPath`/`WindowsUPath` maintain full stdlib compatibility 4. **Flexibility**: Easy to add new filesystem implementations 5. **Future-Proof**: Ready for potential stdlib integration of pathlib-abc ### Writing Filesystem-Agnostic Code Use pathlib-abc types to write code that works with both `Path` and `UPath`: ```python from upath.types import ReadablePath, WritablePath def process_file(input_path: ReadablePath, output_path: WritablePath) -> None: """Works with Path, UPath, or any ReadablePath/WritablePath implementation.""" data = input_path.read_text() processed = data.upper() output_path.write_text(processed) # Works with stdlib Path from pathlib import Path process_file(Path("input.txt"), Path("output.txt")) # Works with UPath for cloud storage from upath import UPath process_file( UPath("s3://input-bucket/data.txt", anon=True), UPath("s3://output-bucket/result.txt") ) # Mix local and remote process_file( UPath("https://example.com/data.txt"), Path("/tmp/result.txt") ) ``` ## Learn More - **pathlib concepts**: See [pathlib.md](pathlib.md) for details on the pathlib API - **fsspec backends**: See [filesystems.md](fsspec.md) for information about available filesystems - **API reference**: Check the [API documentation](../api/index.md) for complete method details - **fsspec details**: Visit [fsspec documentation](https://filesystem-spec.readthedocs.io/) for filesystem-specific options universal_pathlib-0.3.10/docs/contributing.md000066400000000000000000000260271514661127100213020ustar00rootroot00000000000000# Contributing to Universal Pathlib :heart: Thank you for your interest in contributing to Universal Pathlib! We're excited to have you here. :tada: This project thrives on community contributions, and we welcome all forms of participation - whether you're reporting bugs, suggesting features, improving documentation, or contributing code. --- ## Why Contribute? :sparkles: Universal Pathlib is an open-source project that helps developers work seamlessly with different filesystems. By contributing, you: - :handshake: **Help the community** - Your contributions make it easier for others to work with cloud storage and remote filesystems - :books: **Learn and grow** - Get hands-on experience with filesystems, Python internals, and testing - :star2: **Build your profile** - Showcase your work in an active, project - :people_holding_hands: **Join a welcoming community** - Work with friendly maintainers and contributors !!! tip "First Time Contributing?" Don't worry! We're here to help. Everyone was a first-time contributor once, and we're happy to guide you through the process. --- ## Ways to Contribute :gift: There are many ways to contribute to Universal Pathlib:
- :bug: **Report Bugs** --- Found something broken? Let us know! Clear bug reports help us fix issues quickly. [:octicons-arrow-right-24: Report a bug](https://github.com/fsspec/universal_pathlib/issues/new) - :bulb: **Suggest Features** --- Have an idea for improvement? We'd love to hear it! Feature requests help shape the project's future. [:octicons-arrow-right-24: Request a feature](https://github.com/fsspec/universal_pathlib/issues/new) - :memo: **Improve Documentation** --- Spot a typo? Know a better way to explain something? Documentation improvements are always welcome! [:octicons-arrow-right-24: Edit the docs](https://github.com/fsspec/universal_pathlib) - :wrench: **Contribute Code** --- Ready to get your hands dirty? We welcome bug fixes, new features, and performance improvements. [:octicons-arrow-right-24: Development guide](#setting-up-your-development-environment)
--- ## Reporting Bugs :bug: Found a bug? We want to know about it! Here's how to report it effectively: ### Before Reporting 1. **Search existing issues** - Someone might have already reported it 2. **Try the latest version** - The bug might already be fixed 3. **Check the documentation** - Make sure it's actually a bug and not expected behavior ### Creating a Bug Report When filing a bug report, please include: - **Operating system and Python version** - Which OS and Python version are you using? - **Universal Pathlib version** - Which version of the project are you running? - **Filesystem type** - S3, GCS, local, etc.? - **What you did** - Clear steps to reproduce the issue - **What you expected** - What should have happened? - **What actually happened** - What did you see instead? - **Code example** - A minimal reproducible example is extremely helpful! !!! example "Good Bug Report Example" ````markdown **Environment:** - OS: macOS 14.0 - Python: 3.11.5 - universal_pathlib: 0.2.2 - Filesystem: S3 (s3fs 2023.10.0) **Issue:** `UPath.glob()` doesn't match files with spaces in their names on S3. **Reproduction:** ```python from upath import UPath path = UPath("s3://my-bucket/") # This file exists: "my file.txt" list(path.glob("my*.txt")) # Returns empty list ``` **Expected:** Should find "my file.txt" **Actual:** Returns empty list ```` [:octicons-arrow-right-24: Report a bug on GitHub](https://github.com/fsspec/universal_pathlib/issues/new) --- ## Requesting Features :bulb: Have an idea to make Universal Pathlib better? We'd love to hear it! ### Before Requesting 1. **Check existing issues** - Someone might have already suggested it 2. **Think about scope** - Does it fit with the project's goals? 3. **Consider alternatives** - Could it be implemented with existing features? ### Creating a Feature Request When requesting a feature, please explain: - **The problem** - What are you trying to solve? - **Your proposed solution** - How would you like it to work? - **Alternatives considered** - What other approaches did you think about? - **Use case** - When would this feature be useful? !!! example "Good Feature Request Example" ````markdown **Problem:** When working with large directories, I need to filter paths by size before loading them into memory. **Proposed Solution:** Add a `glob_with_info()` method that yields tuples of (path, stat_info). **Use Case:** Processing large S3 buckets where I only want files over 100MB: ```python for path, info in bucket.glob_with_info("**/*.parquet"): if info.st_size > 100_000_000: process(path) ``` **Alternatives:** Currently need to call `stat()` on every path, which is slow. ```` [:octicons-arrow-right-24: Request a feature on GitHub](https://github.com/fsspec/universal_pathlib/issues/new) --- ## Setting Up Your Development Environment :computer: Ready to contribute code? Here's how to set up your development environment. ### Prerequisites You'll need: - **Python 3.9 or higher** - **Git** - For version control - **nox** - For running tests and other tasks ### Installation Steps 1. **Fork and clone the repository** ```bash # Fork on GitHub first, then: git clone https://github.com/YOUR-USERNAME/universal_pathlib.git cd universal_pathlib ``` 2. **Install nox** ```bash uv tool install nox ``` 3. **Verify your setup** ```bash # List available nox sessions nox --list-sessions ``` That's it! You're ready to start developing. !!! tip "Using uv?" If you prefer `uv`, you can use it as well: ```bash uv pip install nox ``` --- ## Running Tests :test_tube: Universal Pathlib has a comprehensive test suite to ensure quality and compatibility. ### Run All Tests ```bash nox ``` This runs the full test suite across multiple Python versions. It may take a while! ### Run Tests for Your Python Version ```bash nox --session=tests ``` This runs tests using your current Python version only. ### Run Specific Test Files ```bash nox --session=tests -- tests/test_core.py ``` ### Run Tests with Coverage ```bash nox --session=tests -- --cov=upath --cov-report=html ``` Then open `htmlcov/index.html` to view the coverage report. ### List All Available Sessions ```bash nox --list-sessions ``` !!! info "Test Requirements" Some tests require additional dependencies (like `s3fs` for S3 tests). Install extras as needed: ```bash pip install -e ".[dev,s3,gcs,azure]" ``` --- ## Code Quality :sparkles: We use automated tools to maintain code quality and consistency. ### Running Linters ```bash nox --session=lint ``` This runs: - **ruff** - Fast Python linter - **mypy** - Type checking - **Code formatting checks** ### Type Checking ```bash nox --session=type_checking nox --session=typesafety ``` !!! tip "Pre-commit Hooks" Consider setting up pre-commit hooks to automatically check your code: ```bash pip install pre-commit pre-commit install ``` --- ## Making Changes :wrench: Here's the workflow for contributing code: ### 1. Create a Branch ```bash git checkout -b feature/my-awesome-feature # or git checkout -b fix/bug-description ``` ### 2. Make Your Changes - Write clear, readable code - Follow existing code style - Add tests for new functionality - Update documentation as needed ### 3. Test Your Changes ```bash # Run tests nox --session=tests # Run linters nox --session=lint ``` ### 4. Commit Your Changes ```bash git add . git commit -m "Add feature: brief description of what you did" ``` !!! tip "Good Commit Messages" - Start with a verb: "Add", "Fix", "Update", "Remove" - Be concise but descriptive - Reference issues: "Fix #123: Description" ### 5. Push and Create a Pull Request ```bash git push origin feature/my-awesome-feature ``` Then open a pull request on GitHub! --- ## Pull Request Guidelines :rocket: Your pull request should: - ✅ **Pass all tests** - The test suite must pass without errors - ✅ **Maintain coverage** - Add tests for new functionality - ✅ **Update documentation** - Document any API changes - ✅ **Follow code style** - Pass linting checks - ✅ **Have a clear description** - Explain what and why ### Pull Request Template When creating a PR, please include: ```markdown ## Description Brief description of changes ## Type of Change - [ ] Bug fix - [ ] New feature - [ ] Documentation update - [ ] Performance improvement ## Testing How was this tested? ## Checklist - [ ] Tests pass locally - [ ] Linting passes - [ ] Documentation updated - [ ] CHANGELOG.md updated (if applicable) ``` !!! success "Don't Worry About Perfection" It's okay to submit a work-in-progress PR! We can iterate and improve it together. Just mark it as a draft if it's not ready for review. --- ## Development Tips :bulb: ### Debugging Tests ```bash # Run specific test with verbose output nox --session=tests -- tests/test_core.py::test_name -v # Drop into debugger on failure nox --session=tests -- --pdb ``` --- ## Getting Help :question: Need help? We're here for you! - **GitHub Issues** - Ask questions, report bugs, suggest features - **Documentation** - Check the docs for guides and examples - **Code of Conduct** - Read our [Code of Conduct](https://github.com/fsspec/universal_pathlib/blob/main/CODE_OF_CONDUCT.rst) !!! info "Response Time" We're a volunteer-driven project. We'll try to respond quickly, but please be patient if it takes a few days. --- ## Recognition :star: We appreciate all contributions! Contributors are: - Listed in the project's contributor graph - Mentioned in release notes for significant contributions - Part of a welcoming, collaborative community Thank you for making Universal Pathlib better! :heart: --- ## Quick Links :link:
- :octicons-mark-github-16: **GitHub Repository** --- View the source code and contribute [:octicons-arrow-right-24: fsspec/universal_pathlib](https://github.com/fsspec/universal_pathlib) - :octicons-issue-opened-16: **Issue Tracker** --- Report bugs and request features [:octicons-arrow-right-24: View issues](https://github.com/fsspec/universal_pathlib/issues) - :octicons-git-pull-request-16: **Pull Requests** --- Submit your contributions [:octicons-arrow-right-24: Open a PR](https://github.com/fsspec/universal_pathlib/pulls) - :octicons-code-of-conduct-16: **Code of Conduct** --- Read our community guidelines [:octicons-arrow-right-24: View code of conduct](https://github.com/fsspec/universal_pathlib/blob/main/CODE_OF_CONDUCT.rst)
---
**Ready to contribute?** :rocket: [Create an Issue](https://github.com/fsspec/universal_pathlib/issues/new){ .md-button .md-button--primary } [Open a Pull Request](https://github.com/fsspec/universal_pathlib/compare){ .md-button }
universal_pathlib-0.3.10/docs/css/000077500000000000000000000000001514661127100170325ustar00rootroot00000000000000universal_pathlib-0.3.10/docs/css/extra.css000066400000000000000000000003411514661127100206650ustar00rootroot00000000000000:root { --md-primary-fg-color: #4361EE; scrollbar-gutter: stable; overflow-y: scroll; } .md-typeset table:not([class]) th:not(:first-child) { min-width: 1em; padding-left: 0.8em; padding-right: 0.8em; } universal_pathlib-0.3.10/docs/index.md000066400000000000000000000153661514661127100177060ustar00rootroot00000000000000 ![universal pathlib logo](assets/logo-text.svg){: #upath-logo } [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/universal_pathlib)](https://pypi.org/project/universal_pathlib/) [![PyPI - License](https://img.shields.io/pypi/l/universal_pathlib)](https://github.com/fsspec/universal_pathlib/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/universal_pathlib.svg)](https://pypi.org/project/universal_pathlib/) [![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/universal_pathlib?label=conda)](https://anaconda.org/conda-forge/universal_pathlib) [![Docs](https://readthedocs.org/projects/universal-pathlib/badge/?version=latest)](https://universal-pathlib.readthedocs.io/en/latest/?badge=latest) [![Tests](https://github.com/fsspec/universal_pathlib/actions/workflows/tests.yml/badge.svg)](https://github.com/fsspec/universal_pathlib/actions/workflows/tests.yml) [![GitHub issues](https://img.shields.io/github/issues/fsspec/universal_pathlib)](https://github.com/fsspec/universal_pathlib/issues) [![Codestyle black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Changelog](https://img.shields.io/badge/changelog-Keep%20a%20Changelog-%23E05735)](./changelog.md) --- **Universal Pathlib** is a Python library that extends the [`pathlib_abc.JoinablePath`][pathlib_abc], [`pathlib_abc.Readable`][pathlib_abc], and [`pathlib_abc.Writable`][pathlib_abc] API to give you a unified, Pythonic interface for working with files, whether they're on your local machine, in S3, on GitHub, or anywhere else. Built on top of [`filesystem_spec`][fsspec], it brings the convenienve of a [`pathlib.Path`][pathlib]-like interface to cloud storage, remote filesystems, and more! :sparkles: [pathlib_abc]: https://github.com/barneygale/pathlib-abc [pathlib]: https://docs.python.org/3/library/pathlib.html [fsspec]: https://filesystem-spec.readthedocs.io/en/latest/intro.html --- If you enjoy working with Python's [pathlib][pathlib] objects to operate on local file system paths, universal pathlib provides the same interface for many supported [ filesystem_spec ][fsspec] implementations, from cloud-native object storage like `Amazon's S3 Storage`, `Google Cloud Storage`, `Azure Blob Storage`, to `http`, `sftp`, `memory` stores, and many more... If you're familiar with [ filesystem_spec ][fsspec], then universal pathlib provides a convenient way to handle the path, protocol and storage options of a object stored on a fsspec filesystem in a single container (`upath.UPath`). And it further provides a pathlib interface to do path operations on the fsspec urlpath. The great part is, if you're familiar with the [pathlib.Path][pathlib] API, you can immediately switch from working with local paths to working on remote and virtual filesystem by simply using the `UPath` class: === "The Problem" ```python # Local files: use pathlib from pathlib import Path local_file = Path("data/file.txt") content = local_file.read_text() # S3 files: use boto3/s3fs import boto3 s3 = boto3.client('s3') obj = s3.get_object(Bucket='bucket', Key='data/file.txt') content = obj['Body'].read().decode('utf-8') # Different APIs, different patterns 😫 ``` === "The Solution" ```python # All files: use UPath! ✨ from upath import UPath local_file = UPath("data/file.txt") s3_file = UPath("s3://bucket/data/file.txt") # Same API everywhere! 🎉 content = local_file.read_text() content = s3_file.read_text() ``` [Learn more about why you should use Universal Pathlib →](why.md){ .md-button } --- ## Quick Start :rocket: ### Installation ```bash pip install universal-pathlib ``` !!! tip "Installing for specific filesystems" To use cloud storage or other remote filesystems, install the necessary fsspec extras: ```bash pip install "universal-pathlib" "fsspec[s3,gcs,azure]" ``` See the [Installation Guide](install.md) for more details. ### TL;DR Examples ```python from upath import UPath # Works with local paths local_path = UPath("documents/notes.txt") local_path.write_text("Hello, World!") print(local_path.read_text()) # "Hello, World!" # Works with S3 s3_path = UPath("s3://my-bucket/data/processed/results.csv") if s3_path.exists(): data = s3_path.read_text() # Works with HTTP http_path = UPath("https://example.com/data/file.json") if http_path.exists(): content = http_path.read_bytes() # Works with many more! 🌟 ``` --- ## Currently supported filesystems - :fontawesome-solid-folder: `file:` and `local:` Local filesystem - :fontawesome-solid-memory: `memory:` Ephemeral filesystem in RAM - :fontawesome-brands-microsoft: `az:`, `adl:`, `abfs:` and `abfss:` Azure Storage _(requires `adlfs`)_ - :fontawesome-solid-database: `data:` RFC 2397 style data URLs _(requires `fsspec>=2023.12.2`)_ - :fontawesome-solid-network-wired: `ftp:` FTP filesystem - :fontawesome-brands-github: `github:` GitHub repository filesystem - :fontawesome-solid-globe: `http:` and `https:` HTTP(S)-based filesystem - :fontawesome-solid-server: `hdfs:` Hadoop distributed filesystem - :fontawesome-brands-google: `gs:` and `gcs:` Google Cloud Storage _(requires `gcsfs`)_ - :simple-huggingface: `hf:` Hugging Face Hub _(requires `huggingface_hub`)_ - :fontawesome-brands-aws: `s3:` and `s3a:` AWS S3 _(requires `s3fs`)_ - :fontawesome-solid-network-wired: `sftp:` and `ssh:` SFTP and SSH filesystems _(requires `paramiko`)_ - :fontawesome-solid-share-nodes: `smb:` SMB filesystems _(requires `smbprotocol`)_ - :fontawesome-solid-cloud: `webdav:`, `webdav+http:` and `webdav+https:` WebDAV _(requires `webdav4[fsspec]`)_ !!! info "Untested Filesystems" Other fsspec-compatible filesystems likely work through the default implementation. If you encounter issues, please [report it our issue tracker](https://github.com/fsspec/universal_pathlib/issues)! We're happy to add official support! --- ## Getting Help :question: Need help? We're here for you! - :fontawesome-brands-github: [GitHub Issues](https://github.com/fsspec/universal_pathlib/issues) - Report bugs or request features - :material-book-open-variant: [Documentation](https://universal-pathlib.readthedocs.io/) - You're reading it! !!! tip "Before Opening an Issue" Please check if your question has already been answered in the documentation or existing issues. --- ## License :page_with_curl: Universal Pathlib is distributed under the [MIT license](https://github.com/fsspec/universal_pathlib/blob/main/LICENSE), making it free and open source software. Use it freely in your projects! ---
**Ready to get started?** [Install Now](install.md){ .md-button .md-button--primary }
universal_pathlib-0.3.10/docs/install.md000066400000000000000000000043031514661127100202320ustar00rootroot00000000000000 # Installation :package: Getting started with `universal-pathlib` is easy! Choose your preferred package manager below and you'll be working with cloud storage in minutes. ## Quick Install === "uv" ```bash uv add universal-pathlib ``` === "pip" ```bash python -m pip install universal-pathlib ``` === "conda" ```bash conda install -c conda-forge universal-pathlib ``` That's it! You now have `universal-pathlib` installed. :tada: ## Filesystem-Specific Dependencies While `universal-pathlib` comes with `fsspec` out of the box, **some filesystems require additional packages**. Don't worry—installing them is straightforward! For example, to work with **AWS S3**, you'll need to install `s3fs`: ```bash pip install s3fs # or better yet, use fsspec extras: pip install "fsspec[s3]" ``` Here are some common filesystem extras you might need: | Filesystem | Install Command | |------------|----------------| | **AWS S3** | `pip install "fsspec[s3]"` | | **Google Cloud Storage** | `pip install "fsspec[gcs]"` | | **Azure Blob Storage** | `pip install "fsspec[azure]"` | | **HTTP/HTTPS** | `pip install "fsspec[http]"` | | **GitHub** | `pip install "fsspec[github]"` | | **SSH/SFTP** | `pip install "fsspec[ssh]"` | ## Adding to Your Project When adding `universal-pathlib` to your project, specify the filesystem extras you need. Here's a `pyproject.toml` example for a project using **S3** and **HTTP**: ```toml [project] name = "myproject" requires-python = ">=3.9" dependencies = [ "universal_pathlib>=0.3.10", "fsspec[s3,http]", # Add the filesystems you need ] ``` !!! tip "Complete List of Filesystem Extras" For a complete overview of all available filesystem extras and their dependencies, check out the [fsspec pyproject.toml file][fsspec-pyproject-toml]. It includes extras for: - Cloud storage (S3, GCS, Azure, etc.) - Remote protocols (HTTP, FTP, SSH, etc.) - Specialized systems (HDFS, WebDAV, SMB, etc.) [fsspec-pyproject-toml]: https://github.com/fsspec/filesystem_spec/blob/master/pyproject.toml#L26 ---
**Ready to get started?** Learn about [Universal Pathlib Concepts](concepts/index.md) :rocket:
universal_pathlib-0.3.10/docs/migration.md000066400000000000000000000203501514661127100205550ustar00rootroot00000000000000 # Migration Guide This guide helps you migrate to newer versions of universal-pathlib. !!! warning Please open an issue if you run into issues when migrating to a newer UPath version and this guide is missing information. ## Migrating to v0.3.0 Version `0.3.0` introduced a breaking change to fix a longstanding bug related to `os.PathLike` protocol compliance. This change affects how UPath instances work with standard library functions that expect local filesystem paths. ### Background: PathLike Protocol and Local Filesystem Paths In Python, `os.PathLike` objects and `pathlib.Path` subclasses represent **local filesystem paths**. The standard library functions like `os.remove()`, `shutil.copy()`, and similar expect paths that point to the local filesystem. However, UPath implementations like `S3Path` or `MemoryPath` do not represent local filesystem paths and should not be treated as such. Prior to `v0.3.0`, all UPath instances incorrectly implemented `os.PathLike`, which could lead to runtime errors when non-local paths were passed to functions expecting local paths. Starting with `v0.3.0`, only local UPath implementations (`PosixUPath`, `WindowsUPath`, and `FilePath`) implement `os.PathLike`. ### Migration Strategies If your code passes UPath instances to functions expecting `os.PathLike` objects, you have several options: **Option 1: Explicitly Request a Local Path** (Recommended) ```python import os from upath import UPath # Explicitly specify the file:// protocol to get a FilePath instance path = UPath(__file__, protocol="file") assert isinstance(path, os.PathLike) # True # Now you can safely use it with os functions os.remove(path) ``` **Option 2: Use UPath's Filesystem Operations** ```python from upath import UPath # Works for any UPath implementation, not just local paths path = UPath("s3://bucket/file.txt") path.unlink() # UPath's native unlink method ``` **Option 3: Use Type Checking with upath.types** For code that needs to work with different path types, use the type hints from `upath.types` to properly specify your requirements: ```python import os from upath import UPath from upath.types import ( JoinablePathLike, ReadablePathLike, WritablePathLike, ) def read_only_local_file(path: os.PathLike) -> str: """Read a file on the local filesystem.""" with open(path) as f: return f.read() def write_only_local_file(path: os.PathLike, content: str) -> None: """Write to a file on the local filesystem.""" with open(path, 'w') as f: f.write(content) def read_any_file(path: ReadablePathLike) -> str: """Read a file on any filesystem.""" return UPath(path).read_text() def write_any_file(path: WritablePathLike, content: str) -> None: """Write a file on any filesystem.""" UPath(path).write_text(content) ``` ### Example: Incorrect Code That Would Fail The following example shows code that would incorrectly work in `v0.2.x` but properly fail in `v0.3.0`: ```python import os from upath import UPath # This creates a MemoryPath, which is not a local filesystem path path = UPath("memory:///file.txt") # In v0.2.x this would incorrectly accept the path and fail at runtime # In v0.3.0 this correctly fails at type-check time os.remove(path) # TypeError: expected str, bytes or os.PathLike, not MemoryPath ``` ### Working with Polars and Object Store When using UPath with [Polars](https://pola.rs/), be aware that Polars uses Rust's [object-store](https://docs.rs/object_store/) library instead of fsspec. This requires special handling to preserve storage options. !!! warning "Don't Rely on Implicit String Conversion" Avoid passing UPath instances directly to functions that implicitly cast them to strings via `os.fspath()` or `os.path.expanduser()`. This loses storage options and can lead to authentication failures. **Problematic Pattern:** ```python import polars as pl from upath import UPath # This loses storage_options when implicitly converted to string! path = UPath('s3://bucket/file.parquet', anon=True) df = pl.read_parquet(path) # anon=True is lost! ``` **Recommended Approaches:** **Option 1: Use fsspec/s3fs as the Backend** (via file handle) ```python import polars as pl from upath import UPath path = UPath("s3://bucket/file.parquet", anon=True) # Open file handle with fsspec, preserving storage_options df = pl.scan_parquet(path.open('rb')) ``` **Option 2: Use Polars' Native Rust Backend** (with object-store options) ```python import polars as pl from upath import UPath path = UPath("s3://bucket/file.parquet", key="ACCESS_KEY", secret="SECRET_KEY") # Convert fsspec storage_options to object-store format object_store_options = { "aws_access_key_id": path.storage_options.get("key"), "aws_secret_access_key": path.storage_options.get("secret"), # Add other options as needed } df = pl.scan_parquet(path.as_uri(), storage_options=object_store_options) ``` !!! info "Storage Options Mapping" Polars uses [object-store configuration keys](https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html), which differ from fsspec's naming: | fsspec/s3fs | object-store | |-------------|--------------| | `key` | `aws_access_key_id` | | `secret` | `aws_secret_access_key` | | `endpoint_url` | `aws_endpoint` | | `region_name` | `aws_region` | See also: [pola-rs/polars#24921](https://github.com/pola-rs/polars/issues/24921) ### Extending UPath via `_protocol_dispatch=False` If you previously used `_protocol_dispatch=False` to enable extension of the UPath API, we now recommend subclassing `upath.extensions.ProxyUPath`. See the advanced usage documentation for examples. ## Migrating to v0.2.0 ### _FSSpecAccessor Subclasses with Custom Filesystem Access Methods If you implemented a custom accessor subclass, override the corresponding `UPath` methods in your subclass directly: ```python # OLD: v0.1.x from upath.core import UPath, _FSSpecAccessor class MyAccessor(_FSSpecAccessor): def exists(self, path, **kwargs): # custom logic pass class MyPath(UPath): _default_accessor = MyAccessor # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): def exists(self, *, follow_symlinks=True): # custom logic pass ``` ### _FSSpecAccessor Subclasses with Custom `__init__` Method If you implemented a custom `__init__` method for your accessor subclass to customize fsspec filesystem instantiation, use the new `_fs_factory` or `_parse_storage_options` classmethods: ```python # OLD: v0.1.x import fsspec from upath.core import UPath, _FSSpecAccessor class MyAccessor(_FSSpecAccessor): def __init__(self, parsed_url, **kwargs): # custom filesystem setup super().__init__(parsed_url, **kwargs) class MyPath(UPath): _default_accessor = MyAccessor # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): @classmethod def _fs_factory(cls, protocol, storage_options): # custom filesystem setup return super()._fs_factory(protocol, storage_options) ``` ### Access to `._accessor` The `_accessor` attribute and the `_FSSpecAccessor` class are deprecated. Use `UPath().fs` to access the underlying filesystem: ```python # OLD: v0.1.x from upath import UPath class MyPath(UPath): def mkdir(self, mode=0o777, parents=False, exist_ok=False): self._accessor.mkdir(self.path, **kwargs) # NEW: v0.2.0+ from upath import UPath class MyPath(UPath): def mkdir(self, mode=0o777, parents=False, exist_ok=False): self.fs.mkdir(self.path, **kwargs) ``` ### Private Attributes to Public API Move from deprecated private attributes to public API: | Deprecated | v0.2.0+ | |:-----------|:--------| | `UPath()._path` | `UPath().path` | | `UPath()._kwargs` | `UPath().storage_options` | | `UPath()._drv` | `UPath().drive` | | `UPath()._root` | `UPath().root` | | `UPath()._parts` | `UPath().parts` | ### Access to `._url` The `._url` attribute will likely be deprecated once `UPath()` has support for URI fragments and query parameters through a public API. If you need this functionality, please open an issue. ### Custom Path Flavours The `_URIFlavour` class was removed. The internal `FSSpecFlavour` in `upath._flavour` is experimental. If you need custom path flavour functionality, please open an issue to discuss maintainable solutions. universal_pathlib-0.3.10/docs/usage.md000066400000000000000000000217361514661127100177010ustar00rootroot00000000000000 # Usage Guide Welcome! This guide will help you get started with Universal Pathlib. If you already know Python's `pathlib`, you'll feel right at home—`UPath` works the same way, but for any filesystem. ## Getting Started First, import what you need: ```python from upath import UPath ``` That's it! You're ready to work with files anywhere. --- ## Common Tasks ### How do I work with local files? UPath behaves just like `pathlib.Path` for local files: ```python import pathlib from tempfile import NamedTemporaryFile # Create a local path tmp = NamedTemporaryFile() local_path = UPath(tmp.name) # It's a regular pathlib object! assert isinstance(local_path, (pathlib.PosixPath, pathlib.WindowsPath)) ``` Want to rely on fsspec for local filesystems too? Use a `file://` URI: ```python local_uri = local_path.absolute().as_uri() # Result: 'file:///tmp/tmpXXXXXX' local_upath = UPath(local_uri) # Now it uses fsspec backend assert isinstance(local_upath, UPath) ``` ### How do I connect to cloud storage or remote filesystems? Just use the appropriate URI scheme. UPath will automatically connect to the right filesystem: ```python # Amazon S3 s3path = UPath("s3://my-bucket/data/file.txt") # Google Cloud Storage gcs_path = UPath("gs://my-bucket/data.csv") # Azure Blob Storage az_path = UPath("az://container/blob.parquet") # Hugging Face Hub hf_path = UPath("hf://datasets/username/dataset-name/data.csv") # GitHub repositories gh_path = UPath("github://fsspec:universal_pathlib@main/") ``` You can also pass connection options as keyword arguments: ```python # GitHub with explicit parameters ghpath = UPath('github:/', org='fsspec', repo='universal_pathlib', sha='main') ``` Or use the `protocol` keyword argument to select the implementation: ```python # Using protocol kwarg for S3 s3path = UPath('my-bucket/data/file.txt', protocol='s3') # Using protocol kwarg for Azure with configuration azpath = UPath( 'container/blob.parquet', protocol='az', account_name='myaccount', account_key='mykey' ) ``` ### How do I read a file? Same as regular pathlib—just call `read_text()` or `read_bytes()`: ```python # Read from GitHub ghpath = UPath('github://fsspec:universal_pathlib@main/') readme = ghpath / "README.md" first_line = readme.read_text().splitlines()[0] print(first_line) ``` For more control, use `open()`: ```python s3path = UPath("s3://spacenet-dataset/LICENSE.md", anon=True) with s3path.open("rt", encoding="utf-8") as f: print(f.read(22)) ``` ### How do I list files in a directory? Use `iterdir()` to iterate through contents: ```python s3path = UPath("s3://spacenet-dataset", anon=True) for item in s3path.iterdir(): if item.is_file(): print(item) break ``` ### How do I work with path components? All the familiar `pathlib` attributes work: ```python ghpath = UPath('github://fsspec:universal_pathlib@main/') readme = ghpath / "README.md" # Get different parts of the path print(readme.name) # "README.md" print(readme.stem) # "README" print(readme.suffix) # ".md" print(readme.parent) # The parent directory # Get the full path as a string print(str(readme)) # Get just the path without the scheme print(readme.path) ``` ### How do I join paths? Use the `/` operator, just like pathlib: ```python base = UPath("s3://my-bucket") data_dir = base / "data" / "processed" csv_file = data_dir / "output.csv" ``` ### How do I check if a file exists? Use `exists()`, `is_file()`, or `is_dir()`: ```python s3path = UPath("s3://spacenet-dataset/LICENSE.md") if s3path.exists(): print("File exists!") if s3path.is_file(): print("It's a file!") ``` ### How does UPath handle S3 prefixes that don't exist? S3 prefixes aren't traditional POSIX directories. UPath follows `pathlib` conventions—checking a non-existent path returns `False`: ```python from upath import UPath # Non-existent bucket returns False, just like pathlib fake_path = UPath("s3://bucket-that-doesnt-exist/my-dir/") print(fake_path.is_dir()) # False ``` This matches standard `pathlib` behavior: ```python import pathlib # pathlib also returns False for non-existent paths assert pathlib.Path('/path/that/does/not/exist').is_dir() is False assert pathlib.Path('/path/that/does/not/exist').exists() is False ``` !!! warning "Authentication Required" If the bucket exists but you lack credentials, an authentication exception will be raised: ```python # This bucket exists but requires authentication s3_path = UPath("s3://my-private-bucket/data/") s3_path.is_dir() # Raises authentication exception ``` ### How do I search for files matching a pattern? Use `glob()` for pattern matching: ```python s3path = UPath("s3://spacenet-dataset") # Find all .TIF files in a specific directory paris_data = s3path / "AOIs" / "AOI_3_Paris" for tif_file in paris_data.glob("**.TIF"): print(tif_file) break ``` !!! note "Glob Syntax" The glob syntax follows [fsspec conventions](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem.glob), which may differ slightly from pathlib's glob. --- ## Advanced Usage ### How do I pass credentials or configuration? Pass them as keyword arguments when creating the path: ```python # S3 with custom endpoint and credentials s3_path = UPath( "s3://my-bucket/file.txt", endpoint_url="https://s3.custom-domain.com", key="ACCESS_KEY", secret="SECRET_KEY" ) # Anonymous access to public buckets public_s3 = UPath("s3://spacenet-dataset", anon=True) ``` ### How do I access the underlying filesystem? UPath gives you access to the fsspec filesystem when you need lower-level control: ```python path = UPath("s3://my-bucket/file.txt") # Access the filesystem object fs = path.fs # Get the path string for use with fsspec path_str = path.path # Get storage options options = path.storage_options # Use fsspec directly if needed with fs.open(path_str, 'rb') as f: data = f.read() ``` ### How do I create a custom UPath for a new filesystem? Let's say you have an fsspec filesystem with protocol `myproto` and the default implementation does not correctly work for `.is_dir()`. You can then subclass `UPath` and register your implementation: ```python from upath import UPath from upath.registry import register_implementation class MyCustomPath(UPath): # fix specific methods if the filesystem is a bit non-standard def is_dir(self, *, follow_symlinks=True): # some special way to check if it's a dir is_dir = ... return is_dir # Register for your protocol register_implementation("myproto", MyCustomPath) # Now it works! my_path = UPath("myproto://server/path") ``` !!! note "don't extend the API in your subclass" You should not extend the API in your UPath subclass. If you want to add new methods please use `upath.extensions.ProxyUPath` as a base class ### How do I add custom methods to UPath? If you need to add domain-specific methods (like `.download()` or `.upload()`), use `ProxyUPath` instead of subclassing `UPath` directly: ```python from upath import UPath from upath.extensions import ProxyUPath class MyCustomPath(ProxyUPath): """A path with extra convenience methods.""" def download(self, local_path): """Download this remote file to a local path.""" local = UPath(local_path) local.write_bytes(self.read_bytes()) return local def get_metadata(self): """Get custom metadata for this file.""" stat = self.stat() return { 'size': stat.st_size, 'modified': stat.st_mtime, 'name': self.name, } # Use it like a regular UPath path = MyCustomPath("s3://my-bucket/data.csv", anon=True) # Access standard UPath methods print(path.exists()) print(path.name) # Use your custom methods metadata = path.get_metadata() path.download("/tmp/data.csv") ``` The key difference: `ProxyUPath` wraps a `UPath` instance and delegates to it, while keeping your custom methods separate from the core pathlib API. --- ## Supported Filesystems Universal Pathlib works with any [fsspec](https://filesystem-spec.readthedocs.io/) filesystem. Here are some popular ones: | Protocol | Filesystem | Install Package | |----------|-----------|-----------------| | `file://` | Local files | _(built-in)_ | | `s3://` | Amazon S3 | `s3fs` | | `gs://`, `gcs://` | Google Cloud Storage | `gcsfs` | | `az://`, `abfs://` | Azure Blob Storage | `adlfs` | | `hf://` | Hugging Face Hub | `huggingface_hub` | | `github://` | GitHub | _(built-in)_ | | `http://`, `https://` | HTTP(S) | _(built-in)_ | | `ssh://`, `sftp://` | SSH/SFTP | `paramiko` | | `hdfs://` | Hadoop HDFS | `pyarrow` | | `smb://` | SMB/Samba | `smbprotocol` | | `webdav://` | WebDAV | `webdav4` | You can see all available implementations programmatically: ```python from fsspec.registry import known_implementations for name, details in sorted(known_implementations.items()): print(f"{name}: {details['class']}") ``` !!! tip "Custom Filesystems" Built a custom fsspec filesystem? UPath will work with it automatically! universal_pathlib-0.3.10/docs/why.md000066400000000000000000000153661514661127100174060ustar00rootroot00000000000000# Why Use Universal Pathlib? :sparkles: If you've ever worked with cloud storage or remote filesystems in Python, you've probably experienced the frustration of juggling different APIs. Universal Pathlib solves this problem elegantly by bringing the beloved `pathlib.Path` interface to *any* filesystem spec filesystem. --- ## The Problem: Filesystem dependent APIs :broken_heart: Let's face it: working with files across different storage backends is messy. ### Example: The Old Way ```python # Local files from pathlib import Path local_file = Path("data/results.csv") with local_file.open('r') as f: data = f.read() # S3 files import boto3 s3 = boto3.resource('s3') obj = s3.Object('my-bucket', 'data/results.csv') data = obj.get()['Body'].read().decode('utf-8') # Azure Blob Storage from azure.storage.blob import BlobServiceClient blob_client = BlobServiceClient.from_connection_string(conn_str) container_client = blob_client.get_container_client('my-container') blob_client = container_client.get_blob_client('data/results.csv') data = blob_client.download_blob().readall().decode('utf-8') # Three different APIs, three different patterns 😫 ``` Each storage backend has its own: - :material-api: **Different API** - Learn a new interface for each service - :material-puzzle: **Different patterns** - Different ways to read, write, and list files - :material-code-braces: **Different imports** - Manage multiple dependencies - :material-hammer-wrench: **Different configurations** - Each with unique setup requirements !!! danger "The Maintenance Nightmare" Want to switch from S3 to GCS? Rewrite your code. Need to support multiple backends? Write wrapper functions. Testing? Mock each service differently. This doesn't scale! --- ## The Solution: One API to Rule Them All :crown: Universal Pathlib provides a single, unified interface that works everywhere: ```python from upath import UPath # Local files local_file = UPath("data/results.csv") # S3 files s3_file = UPath("s3://my-bucket/data/results.csv") # Azure Blob Storage azure_file = UPath("az://my-container/data/results.csv") # Same API everywhere! ✨ for path in [local_file, s3_file, azure_file]: with path.open('r') as f: data = f.read() ``` !!! success "One API, Infinite Possibilities" Write your code once, run it anywhere. Switch backends by changing a URL. Test locally, deploy to the cloud. It just works! :sparkles: --- ## Key Benefits :trophy: ### 1. Familiar and Pythonic :snake: If you know Python's `pathlib`, you already know Universal Pathlib! ```python from upath import UPath # All the familiar pathlib operations path = UPath("s3://bucket/data/file.txt") print(path.name) # "file.txt" print(path.stem) # "file" print(path.suffix) # ".txt" print(path.parent) # UPath("s3://bucket/data") # Path joining output = path.parent / "processed" / "output.csv" # File operations path.write_text("Hello!") content = path.read_text() # Directory operations for item in path.parent.iterdir(): print(item) ``` !!! tip "Zero Learning Curve" Your existing pathlib knowledge transfers directly. No new concepts to learn, no cognitive overhead! ### 2. Write Once, Run Anywhere :earth_americas: Change storage backends without changing code: === "Development (Local)" ```python from upath import UPath def process_data(input_path: str, output_path: str): data_file = UPath(input_path) result_file = UPath(output_path) # Read, process, write data = data_file.read_text() processed = data.upper() result_file.write_text(processed) # Local development process_data("data/input.txt", "data/output.txt") ``` === "Production (S3)" ```python from upath import UPath def process_data(input_path: str, output_path: str): data_file = UPath(input_path) result_file = UPath(output_path) # Same code! Just different paths data = data_file.read_text() processed = data.upper() result_file.write_text(processed) # Production on S3 process_data( "s3://my-bucket/data/input.txt", "s3://my-bucket/data/output.txt" ) ``` === "Testing (Memory)" ```python from upath import UPath def process_data(input_path: str, output_path: str): data_file = UPath(input_path) result_file = UPath(output_path) # Same code! No mocking needed data = data_file.read_text() processed = data.upper() result_file.write_text(processed) # Fast tests with in-memory filesystem process_data( "memory://input.txt", "memory://output.txt" ) ``` !!! success "Truly Portable Code" Your business logic stays clean and your application does not have to care about where the files live anymore. ### 3. Type-Safe and IDE-Friendly :computer: Universal Pathlib includes type hints for excellent IDE support: ```python from upath import UPath from pathlib import Path def process_file(path: UPath | Path) -> str: # Your IDE knows about all methods! if path.exists(): # ✓ Autocomplete content = path.read_text() # ✓ Type checked return content.upper() return "" # Works with both! local_result = process_file(UPath("file.txt")) s3_result = process_file(UPath("s3://bucket/file.txt")) ``` !!! info "Editor Support" Get autocomplete, type checking, and inline documentation in VS Code, PyCharm, and other modern Python IDEs. ### 4. Extensively Tested :test_tube: Universal Pathlib runs a large subset of CPython's pathlib test suite: - :white_check_mark: **Compatibility tested** against standard library pathlib - :white_check_mark: **Cross-version tested** on Python 3.9-3.14 - :white_check_mark: **Integration tested** with real filesystems - :white_check_mark: **Regression tested** for each release !!! quote "Extensively Tested" When we say "pathlib-compatible," we mean it. ### 5. Extensible and Future-Proof :rocket: Built on `fsspec`, the standard for Python filesystem abstractions: ```python # Works with many fsspec filesystems! UPath("s3://...", anon=True) UPath("gs://...", token='anon') UPath("az://...") UPath("https://...") ``` Need a custom filesystem? Implement it once with fsspec, and UPath works automatically! !!! tip "Ecosystem Benefits" Leverage the entire fsspec ecosystem: caching, compression, callback hooks, and more! --- ## Next Steps :footprints: Ready to give Universal Pathlib a try? 1. **[Install Universal Pathlib](install.md)** - Get set up in minutes 2. **[Understand the concepts](concepts/index.md)** - Understand the concepts 3. **[Read the API docs](api/index.md)** - Learn about all the features
[Install Now →](install.md){ .md-button .md-button--primary }
universal_pathlib-0.3.10/environment.yml000066400000000000000000000004651514661127100204060ustar00rootroot00000000000000name: upath channels: - defaults - conda-forge dependencies: - python==3.10 - fsspec # optional - requests - s3fs - jupyter - ipython - pytest - pylint - flake8 - pyarrow - moto - pip - pip: - hadoop-test-cluster - gcsfs - nox universal_pathlib-0.3.10/mkdocs.yml000066400000000000000000000064771514661127100173330ustar00rootroot00000000000000site_name: Universal Pathlib site_description: A universal pathlib implementation for Python strict: true # site_url: !ENV READTHEDOCS_CANONICAL_URL theme: name: 'material' logo: assets/logo-128x128-white.svg favicon: 'assets/favicon.png' palette: - media: "(prefers-color-scheme: light)" toggle: icon: material/lightbulb-outline name: "Switch to dark mode" - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/lightbulb name: "Switch to light mode" features: # - content.tabs.link - content.code.annotate - content.code.copy - announce.dismiss - navigation.tabs extra_css: - css/extra.css repo_name: fsspec/universal_pathlib repo_url: https://github.com/fsspec/universal_pathlib edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn nav: - Home: - Introduction: index.md - Why use Universal Pathlib: why.md - Installation: install.md - Contributing: contributing.md - Changelog: changelog.md - Concepts: - Overview: concepts/index.md - Filesystem Spec: concepts/fsspec.md - Standard Library Pathlib: concepts/pathlib.md - Universal Pathlib: concepts/upath.md - Usage: - Basic Usage: usage.md - API Reference: - Core: api/index.md - Registry: api/registry.md - Implementations: api/implementations.md - Extensions: api/extensions.md - Types: api/types.md - Migration Guide: migration.md markdown_extensions: - tables - attr_list - toc: permalink: true title: Page contents - admonition - pymdownx.details - pymdownx.highlight: pygments_lang_class: true - pymdownx.extra - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.tasklist: custom_checkbox: true - pymdownx.tabbed: alternate_style: true - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format watch: - docs/ - upath/ plugins: - search - mkdocstrings: handlers: python: inventories: - https://docs.python.org/3/objects.inv paths: [.] options: preload_modules: - __future__ - typing - abc - asyncio - pathlib - pathlib_abc - fsspec - upath.types._abc - upath.types - upath.registry - upath.core docstring_style: numpy docstring_section_style: list group_by_category: false members_order: source docstring_options: ignore_init_summary: true docstring_section_style: spacy merge_init_into_class: true show_source: true show_root_heading: true show_root_toc_entry: true allow_inspection: true separate_signature: true show_signature: true show_signature_annotations: true show_signature_type_parameters: true signature_crossrefs: true show_symbol_type_heading: true - exclude: glob: - _plugins/* - __pycache__/* - tests/* - test_*.py hooks: - docs/_plugins/copy_changelog.py universal_pathlib-0.3.10/notebooks/000077500000000000000000000000001514661127100173155ustar00rootroot00000000000000universal_pathlib-0.3.10/notebooks/examples.ipynb000066400000000000000000000362021514661127100222010ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "import pathlib\n", "import warnings\n", "from tempfile import NamedTemporaryFile\n", "\n", "from upath import UPath\n", "\n", "warnings.filterwarnings(action=\"ignore\", message=\"UPath .*\", module=\"upath.core\")" ] }, { "cell_type": "markdown", "metadata": { "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "source": [ "# Local Filesystem\n", "\n", "If you give a local path, UPath defaults to `pathlib.PosixPath` or `pathlib.WindowsPath`, just as `pathlib.Path`." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/tmp/tmpdeaokyh7 \n" ] }, { "data": { "text/plain": [ "PosixPath('/tmp/tmpdeaokyh7')" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tmp = NamedTemporaryFile()\n", "print(tmp.name, type(tmp.name))\n", "local_path = UPath(tmp.name)\n", "assert isinstance(local_path, (pathlib.PosixPath, pathlib.WindowsPath))\n", "local_path" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "source": [ "If you give it a scheme registered with fsspec, it will return a UPath which uses fsspec FileSystem backend" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "local_uri='file:///tmp/tmpdeaokyh7'\n", "local_upath=UPath('file:/tmp/tmpdeaokyh7')\n", "type(local_upath)=\n", "type(local_upath.fs)=\n" ] } ], "source": [ "local_uri = local_path.absolute().as_uri()\n", "print(f\"{local_uri=}\")\n", "\n", "local_upath = UPath(local_uri)\n", "print(f\"{local_upath=}\")\n", "\n", "print(f\"{type(local_upath)=}\")\n", "assert isinstance(local_upath, UPath)\n", "\n", "print(f\"{type(local_upath.fs)=}\")\n", "tmp.close()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "source": [ "# `fsspec` FileSystems\n", "\n", "With `UPath` you can connect to any `fsspec` FileSystem and interact with it in with it as you would with your local filesystem using `pathlib`. Connection arguments can be given in a couple of ways:\n", "\n", "You can give them as keyword arguments as described in the `fsspec` [docs](https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations) for each filesystem implementation:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ghpath = UPath('github:/', org='fsspec', repo='universal_pathlib', sha='main')\n", "assert ghpath.exists()\n", "ghpath.fs" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": { "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "source": [ "Or define them in the path/url, in which case they will be appropriately parsed:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "UPath('github://fsspec:universal_pathlib@main/')" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ghpath = UPath('github://fsspec:universal_pathlib@main/')\n", "ghpath" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "With a `UPath` object instantiated, you can now interact with the paths with the usual `pathlib.Path` API." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "github://fsspec:universal_pathlib@main/.flake8\n" ] } ], "source": [ "for p in ghpath.iterdir():\n", " print(p)\n", " break" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "All the standard path methods and attributes of [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) are available too:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "UPath('github://fsspec:universal_pathlib@main/README.md')" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "readme_path = ghpath / \"README.md\"\n", "readme_path" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "To get the full path as a string use:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'github://fsspec:universal_pathlib@main/README.md'" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "str(readme_path)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "You can also use the path attribute to get just the path:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'/README.md'" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# path attribute added\n", "readme_path.path" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "('README.md', 'README', '.md')" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "readme_path.name, readme_path.stem, readme_path.suffix" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'# Universal Pathlib'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "readme_path.read_text().splitlines()[0]" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "s3path = UPath(\"s3://spacenet-dataset\")" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "autoscroll": false, "ein.hycell": false, "ein.tags": "worksheet-0", "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "s3://spacenet-dataset/LICENSE.md\n" ] } ], "source": [ "for p in s3path.iterdir():\n", " if p.is_file():\n", " print(p)\n", " break" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "You can chain paths with the `/` operator and any methods." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(s3path / \"LICENSE.md\").exists()" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The \"SpaceNet Dataset\"\n" ] } ], "source": [ "with (s3path / \"LICENSE.md\").open(\"rt\", encoding=\"utf-8\") as f:\n", " print(f.read(22))" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The `glob` method is also available for most filesystems. Note the syntax here is as detailed in `fsspec` [docs](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem.glob), rather than that of `pathlib`." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "s3://spacenet-dataset/AOIs/AOI_3_Paris/MS/16FEB29111913-M2AS_R01C1-055649178040_01_P001.TIF\n" ] } ], "source": [ "for p in (s3path / \"AOIs\" / \"AOI_3_Paris\").glob(\"**.TIF\"):\n", " print(p)\n", " break" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### Works with fsspec filesystems\n", "\n", "Some filesystems may require additional packages to be installed.\n", "\n", "Check out some of the known implementations:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "| Name | Class |\n", "| --- | --- |\n", "| abfs | adlfs.AzureBlobFileSystem |\n", "| abfss | adlfs.AzureBlobFileSystem |\n", "| adl | adlfs.AzureDatalakeFileSystem |\n", "| arrow_hdfs | fsspec.implementations.arrow.HadoopFileSystem |\n", "| asynclocal | morefs.asyn_local.AsyncLocalFileSystem |\n", "| az | adlfs.AzureBlobFileSystem |\n", "| blockcache | fsspec.implementations.cached.CachingFileSystem |\n", "| cached | fsspec.implementations.cached.CachingFileSystem |\n", "| dask | fsspec.implementations.dask.DaskWorkerFileSystem |\n", "| dbfs | fsspec.implementations.dbfs.DatabricksFileSystem |\n", "| dir | fsspec.implementations.dirfs.DirFileSystem |\n", "| dropbox | dropboxdrivefs.DropboxDriveFileSystem |\n", "| dvc | dvc.api.DVCFileSystem |\n", "| file | fsspec.implementations.local.LocalFileSystem |\n", "| filecache | fsspec.implementations.cached.WholeFileCacheFileSystem |\n", "| ftp | fsspec.implementations.ftp.FTPFileSystem |\n", "| gcs | gcsfs.GCSFileSystem |\n", "| gdrive | gdrivefs.GoogleDriveFileSystem |\n", "| generic | fsspec.generic.GenericFileSystem |\n", "| git | fsspec.implementations.git.GitFileSystem |\n", "| github | fsspec.implementations.github.GithubFileSystem |\n", "| gs | gcsfs.GCSFileSystem |\n", "| hdfs | fsspec.implementations.arrow.HadoopFileSystem |\n", "| hf | huggingface_hub.HfFileSystem |\n", "| http | fsspec.implementations.http.HTTPFileSystem |\n", "| https | fsspec.implementations.http.HTTPFileSystem |\n", "| jlab | fsspec.implementations.jupyter.JupyterFileSystem |\n", "| jupyter | fsspec.implementations.jupyter.JupyterFileSystem |\n", "| libarchive | fsspec.implementations.libarchive.LibArchiveFileSystem |\n", "| memory | fsspec.implementations.memory.MemoryFileSystem |\n", "| oci | ocifs.OCIFileSystem |\n", "| oss | ossfs.OSSFileSystem |\n", "| reference | fsspec.implementations.reference.ReferenceFileSystem |\n", "| root | fsspec_xrootd.XRootDFileSystem |\n", "| s3 | s3fs.S3FileSystem |\n", "| s3a | s3fs.S3FileSystem |\n", "| sftp | fsspec.implementations.sftp.SFTPFileSystem |\n", "| simplecache | fsspec.implementations.cached.SimpleCacheFileSystem |\n", "| smb | fsspec.implementations.smb.SMBFileSystem |\n", "| ssh | fsspec.implementations.sftp.SFTPFileSystem |\n", "| tar | fsspec.implementations.tar.TarFileSystem |\n", "| wandb | wandbfs.WandbFS |\n", "| webdav | webdav4.fsspec.WebdavFileSystem |\n", "| webhdfs | fsspec.implementations.webhdfs.WebHDFS |\n", "| zip | fsspec.implementations.zip.ZipFileSystem |" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from fsspec.registry import known_implementations\n", "from IPython.display import Markdown, display\n", "\n", "known = [\n", " f\"| {name} | {d['class']} |\" for name, d in sorted(known_implementations.items())\n", "]\n", "known = \"\\n\".join([\"| Name | Class |\\n| --- | --- |\", *known])\n", "display(Markdown(known))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "fsspec", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" }, "name": "Untitled.ipynb", "vscode": { "interpreter": { "hash": "d4d4510d3a243cfb62b62dec561eb2191aad85ef77736fec7cfe79076e15c84c" } } }, "nbformat": 4, "nbformat_minor": 4 } universal_pathlib-0.3.10/noxfile.py000066400000000000000000000121561514661127100173350ustar00rootroot00000000000000"""Automation using nox.""" import glob import os import sys import nox nox.options.reuse_existing_virtualenvs = True nox.options.error_on_external_run = True nox.needs_version = ">=2024.04.15" nox.options.default_venv_backend = "uv" nox.options.sessions = "lint", "tests", "type-checking", "type-safety" locations = ("upath",) running_in_ci = os.environ.get("CI", "") != "" SUPPORTED_PYTHONS = ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] BASE_PYTHON = SUPPORTED_PYTHONS[-3] MIN_PYTHON = SUPPORTED_PYTHONS[0] @(lambda f: f()) def FSSPEC_MIN_VERSION() -> str: """Get the minimum fsspec version boundary from pyproject.toml.""" try: from packaging.requirements import Requirement if sys.version_info >= (3, 11): from tomllib import load as toml_load else: from tomli import load as toml_load except ImportError: raise RuntimeError( "We rely on nox>=2024.04.15 depending on `packaging` and `tomli/tomllib`." " Please report if you see this error." ) with open("pyproject.toml", "rb") as f: pyproject_data = toml_load(f) for requirement in pyproject_data["project"]["dependencies"]: req = Requirement(requirement) if req.name == "fsspec": for specifier in req.specifier: if specifier.operator == ">=": return str(specifier.version) raise RuntimeError("Could not find fsspec minimum version in pyproject.toml") @nox.session(python=SUPPORTED_PYTHONS) def tests(session: nox.Session) -> None: """Run the test suite.""" # workaround in case no aiohttp binary wheels are available if session.python == "3.14": session.env["AIOHTTP_NO_EXTENSIONS"] = "1" session.install(".[tests,dev]", "pydantic>=2.12.0a1") else: session.install(".[tests,dev,dev-third-party]") session.run("uv", "pip", "freeze", silent=not running_in_ci) session.run( "pytest", "-m", "not hdfs", "--cov", "--cov-config=pyproject.toml", *session.posargs, env={"COVERAGE_FILE": f".coverage.{session.python}"}, ) @nox.session(python=MIN_PYTHON, name="tests-minversion") def tests_minversion(session: nox.Session) -> None: session.install(f"fsspec=={FSSPEC_MIN_VERSION}", ".[tests,dev]") session.run("uv", "pip", "freeze", silent=not running_in_ci) session.run( "pytest", "-m", "not hdfs", "--cov", "--cov-config=pyproject.toml", *session.posargs, env={"COVERAGE_FILE": f".coverage.{session.python}"}, ) tests_minversion.__doc__ = f"Run the test suite with fsspec=={FSSPEC_MIN_VERSION}." @nox.session def lint(session: nox.Session) -> None: """Run pre-commit hooks.""" session.install("pre-commit") session.install("-e", ".[tests]") args = *(session.posargs or ("--show-diff-on-failure",)), "--all-files" session.run("pre-commit", "run", *args) @nox.session def safety(session: nox.Session) -> None: """Scan dependencies for insecure packages.""" session.install(".") session.install("safety") session.run("safety", "check", "--full-report") @nox.session def build(session: nox.Session) -> None: """Build sdists and wheels.""" session.install("build", "setuptools", "twine") session.run("python", "-m", "build") dists = glob.glob("dist/*") session.run("twine", "check", *dists, silent=True) @nox.session def develop(session: nox.Session) -> None: """Sets up a python development environment for the project.""" session.run("uv", "venv", external=True) @nox.session(name="type-checking", python=BASE_PYTHON) def type_checking(session): """Run mypy checks.""" session.install("-e", ".[typechecking]") session.run("python", "-m", "mypy") @nox.session(name="type-safety", python=SUPPORTED_PYTHONS) def type_safety(session): """Run typesafety tests.""" session.install("-e", ".[typechecking]") session.run( "python", "-m", "pytest", "-v", "-p", "pytest-mypy-plugins", "--mypy-pyproject-toml-file", "pyproject.toml", "typesafety", *session.posargs, ) @nox.session(name="flavours-upgrade-deps", python=BASE_PYTHON) def upgrade_flavours(session): session.run("uvx", "pur", "-r", "dev/requirements.txt") @nox.session(name="flavours-codegen", python=BASE_PYTHON) def generate_flavours(session): session.install("-r", "dev/requirements.txt") with open("upath/_flavour_sources.py", "w") as target: session.run( "python", "dev/fsspec_inspector/generate_flavours.py", stdout=target, stderr=None, ) @nox.session(name="docs-build", python=BASE_PYTHON) def docs_build(session): """Build the documentation in strict mode.""" session.install("--group=docs", "-e", ".") session.run("mkdocs", "build") @nox.session(name="docs-serve", python=BASE_PYTHON) def docs_serve(session): """Serve the documentation with live reloading.""" session.install("--group=docs", "-e", ".") session.run("mkdocs", "serve", "--no-strict") universal_pathlib-0.3.10/pyproject.toml000066400000000000000000000113621514661127100202310ustar00rootroot00000000000000[build-system] requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "universal_pathlib" license = "MIT" authors = [ {name = "Andrew Fulton", email = "andrewfulton9@gmail.com"}, ] description = "pathlib api extended to use fsspec backends" maintainers = [ {name = "Andreas Poehlmann"}, {name = "Andreas Poehlmann", email = "andreas@poehlmann.io"}, {name = "Norman Rzepka"}, ] requires-python = ">=3.9" dependencies = [ "fsspec >=2024.5.0", "pathlib-abc >=0.5.1,<0.6.0", ] classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Development Status :: 4 - Beta", ] keywords = ["filesystem-spec", "pathlib"] dynamic = ["version", "readme"] [tool.setuptools.dynamic] readme = {file = ["README.md"], content-type = "text/markdown"} [project.optional-dependencies] tests = [ "pytest >=8", "pytest-sugar >=0.9.7", "pytest-cov >=4.1.0", "pytest-mock >=3.12.0", "pylint >=2.17.4", "mypy >=1.10.0", "pydantic >=2", "pytest-mypy-plugins >=3.1.2", "packaging", ] typechecking = [ "mypy >=1.10.0", "pytest-mypy-plugins >=3.1.2", ] dev = [ "fsspec[adl,http,github,gcs,s3,ssh,smb] >=2024.5.0", "s3fs >=2024.5.0", "gcsfs >=2024.5.0", "adlfs >=2024", "huggingface_hub", "webdav4[fsspec]", # testing "moto[s3,server]", "wsgidav", "cheroot", # "hadoop-test-cluster", # "pyarrow", "pyftpdlib", "typing_extensions; python_version<'3.11'", ] dev-third-party = [ "pydantic", "pydantic-settings", ] [project.urls] Homepage = "https://github.com/fsspec/universal_pathlib" Changelog = "https://github.com/fsspec/universal_pathlib/blob/main/CHANGELOG.md" [tool.setuptools] include-package-data = false [tool.setuptools.package-data] upath = ["py.typed"] [tool.setuptools.packages.find] exclude = [ "upath.tests", "upath.tests.*", ] namespaces = false [tool.setuptools_scm] write_to = "upath/_version.py" version_scheme = "post-release" [tool.black] line-length = 88 include = '\.pyi?$' exclude = ''' /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' force-exclude = ''' ( ^/upath/tests/pathlib/_test_support\.py |^/upath/tests/pathlib/test_pathlib_.*\.py ) ''' [tool.isort] profile = "black" known_first_party = ["upath"] force_single_line = true line_length = 88 [tool.pytest.ini_options] addopts = "-ra -m 'not hdfs' -p no:pytest-mypy-plugins" markers = [ "hdfs: mark test as hdfs", "pathlib: mark cpython pathlib tests", ] [tool.coverage.run] branch = true source = ["upath"] [tool.coverage.report] show_missing = true exclude_lines = [ "pragma: no cover", "if __name__ == .__main__.:", "if typing.TYPE_CHECKING:", "if TYPE_CHECKING:", "raise NotImplementedError", "raise AssertionError", "@overload", "except ImportError", ] [tool.mypy] # Error output show_column_numbers = false show_error_codes = true show_error_context = true show_traceback = true pretty = true check_untyped_defs = false # Warnings warn_no_return = true warn_redundant_casts = true warn_unreachable = true files = ["upath"] exclude = "^notebooks|^venv.*|tests.*|^noxfile.py" [[tool.mypy.overrides]] module = "fsspec.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "webdav4.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "pathlib_abc.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "smbprotocol.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "pydantic.*" ignore_missing_imports = true ignore_errors = true [[tool.mypy.overrides]] module = "pydantic_core.*" ignore_missing_imports = true ignore_errors = true [[tool.mypy.overrides]] module = "typing_inspection.*" ignore_errors = true [[tool.mypy.overrides]] module = "annotated_types.*" ignore_errors = true [tool.pylint.format] max-line-length = 88 [tool.pylint.message_control] enable = ["c-extension-no-member", "no-else-return"] [tool.pylint.variables] dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs" [tool.codespell] ignore-words-list = " " [tool.bandit] exclude_dirs = ["tests"] skips = ["B101"] [dependency-groups] docs = [ "mkdocs>=1.6.1", "click!=8.2.2,!=8.3.0", # https://github.com/mkdocs/mkdocs/issues/4032 "mkdocs-material>=9.6.22", "mkdocstrings[python]>=0.30.1", "mkdocs-exclude>=1.0.2", "pymdown-extensions>=10.7.0", "ruff>=0.14.1", ] universal_pathlib-0.3.10/typesafety/000077500000000000000000000000001514661127100175075ustar00rootroot00000000000000universal_pathlib-0.3.10/typesafety/test_upath_interface.yml000066400000000000000000000321411514661127100244330ustar00rootroot00000000000000- case: upath_constructor disable_cache: false main: | from upath import UPath reveal_type(UPath("abc")) # N: Revealed type is "upath.core.UPath" # === special upath attributes and methods ============================ - case: upath_special_protocol disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.protocol) # N: Revealed type is "builtins.str" - case: upath_special_storage_options disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.storage_options) # N: Revealed type is "typing.Mapping[builtins.str, Any]" - case: upath_special_path disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.path) # N: Revealed type is "builtins.str" - case: upath_special_fs disable_cache: false main: | from upath import UPath p = UPath("abc") # todo: this can change once fsspec is typed reveal_type(p.fs) # N: Revealed type is "Any" - case: upath_special_joinuri disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.joinuri("efg")) # N: Revealed type is "upath.core.UPath" - case: upath_special__url disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p._url) # NR: Revealed type is "[Tt]uple\[builtins.str, builtins.str, builtins.str, builtins.str, builtins.str, fallback=urllib.parse.SplitResult\]" # === upath pathlib.PurePath interface ================================ - case: upath_parts disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.parts) # N: Revealed type is "typing.Sequence[builtins.str]" - case: upath_drive disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.drive) # N: Revealed type is "builtins.str" - case: upath_root disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.root) # N: Revealed type is "builtins.str" - case: upath_anchor disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.anchor) # N: Revealed type is "builtins.str" - case: upath_name disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.name) # N: Revealed type is "builtins.str" - case: upath_suffix disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.suffix) # N: Revealed type is "builtins.str" - case: upath_suffixes disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.suffixes) # N: Revealed type is "builtins.list[builtins.str]" - case: upath_stem disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.stem) # N: Revealed type is "builtins.str" - case: upath_hashable disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(hash(p)) # N: Revealed type is "builtins.int" # __fspath__ - case: upath_sortable disable_cache: false main: | from upath import UPath a = UPath("abc") b = UPath("efg") reveal_type(a < b) # N: Revealed type is "builtins.bool" - case: upath_truediv disable_cache: false main: | from upath import UPath a = UPath("abc") / "efg" reveal_type(a) # N: Revealed type is "upath.core.UPath" - case: upath_rtruediv disable_cache: false main: | from upath import UPath a = "efg" / UPath("abc") reveal_type(a) # N: Revealed type is "upath.core.UPath" # __bytes__ - case: upath_as_posix disable_cache: false main: | from upath import UPath reveal_type(UPath("a").as_posix()) # N: Revealed type is "builtins.str" - case: upath_as_uri disable_cache: false main: | from upath import UPath reveal_type(UPath("a").as_uri()) # N: Revealed type is "builtins.str" - case: upath_is_absolute disable_cache: false main: | from upath import UPath reveal_type(UPath("a").is_absolute()) # N: Revealed type is "builtins.bool" - case: upath_is_reserved disable_cache: false main: | from upath import UPath reveal_type(UPath("a").is_reserved()) # N: Revealed type is "builtins.bool" - case: upath_is_relative_to disable_cache: false main: | from upath import UPath reveal_type(UPath("a").is_relative_to("b")) # N: Revealed type is "builtins.bool" - case: upath_match disable_cache: false main: | from upath import UPath reveal_type(UPath("a").match("b")) # N: Revealed type is "builtins.bool" - case: upath_relative_to disable_cache: false main: | from upath import UPath reveal_type(UPath("a").relative_to("b")) # N: Revealed type is "upath.core.UPath" - case: upath_with_name disable_cache: false main: | from upath import UPath reveal_type(UPath("a").with_name("b")) # N: Revealed type is "upath.core.UPath" - case: upath_with_stem disable_cache: false main: | from upath import UPath reveal_type(UPath("a").with_stem("b")) # N: Revealed type is "upath.core.UPath" - case: upath_with_suffix disable_cache: false main: | from upath import UPath reveal_type(UPath("a").with_suffix("b")) # N: Revealed type is "upath.core.UPath" - case: upath_joinpath disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").joinpath("efg")) # N: Revealed type is "upath.core.UPath" - case: upath_parents disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.parents) # N: Revealed type is "typing.Sequence[upath.core.UPath]" - case: upath_parent disable_cache: false main: | from upath import UPath p = UPath("abc") reveal_type(p.parent) # N: Revealed type is "upath.core.UPath" - case: upath_with_segments disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").with_segments("efg")) # N: Revealed type is "upath.core.UPath" # === upath pathlib.Path methods ====================================== - case: upath_cwd disable_cache: false main: | from upath import UPath reveal_type(UPath.cwd()) # N: Revealed type is "upath.core.UPath" - case: upath_stat disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").stat()) # N: Revealed type is "upath.types.StatResultType" - case: upath_chmod disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").chmod(0o777)) # N: Revealed type is "None" - case: upath_exists disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").exists()) # N: Revealed type is "builtins.bool" - case: upath_glob disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").glob("efg")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_rglob disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").rglob("efg")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_is_dir disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_dir()) # N: Revealed type is "builtins.bool" - case: upath_is_file disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_file()) # N: Revealed type is "builtins.bool" - case: upath_is_symlink disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_symlink()) # N: Revealed type is "builtins.bool" - case: upath_is_socket disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_socket()) # N: Revealed type is "builtins.bool" - case: upath_is_fifo disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_fifo()) # N: Revealed type is "builtins.bool" - case: upath_is_block_device disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_block_device()) # N: Revealed type is "builtins.bool" - case: upath_is_char_device disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_char_device()) # N: Revealed type is "builtins.bool" - case: upath_is_junction disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_junction()) # N: Revealed type is "builtins.bool" - case: upath_iterdir disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").iterdir()) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_lchmod disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").lchmod(0o777)) # N: Revealed type is "None" - case: upath_lstat disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").lstat()) # N: Revealed type is "upath.types.StatResultType" - case: upath_mkdir disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").mkdir()) # N: Revealed type is "None" - case: upath_open_default disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").open()) # N: Revealed type is "typing.TextIO" - case: upath_open_text disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").open("r")) # N: Revealed type is "typing.TextIO" - case: upath_open_binary disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").open("rb")) # N: Revealed type is "typing.BinaryIO" - case: upath_is_mount disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").is_mount()) # N: Revealed type is "builtins.bool" - case: upath_readlink disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").readlink()) # N: Revealed type is "upath.core.UPath" - case: upath_rename disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").rename("efg")) # N: Revealed type is "upath.core.UPath" - case: upath_replace disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").replace("efg")) # N: Revealed type is "upath.core.UPath" - case: upath_resolve disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").resolve()) # N: Revealed type is "upath.core.UPath" - case: upath_rmdir disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").rmdir()) # N: Revealed type is "None" - case: upath_symlink_to disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").symlink_to("efg")) # N: Revealed type is "None" - case: upath_hardlink_to disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").hardlink_to("efg")) # N: Revealed type is "None" - case: upath_touch disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").touch()) # N: Revealed type is "None" - case: upath_unlink disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").unlink()) # N: Revealed type is "None" - case: upath_home disable_cache: false main: | from upath import UPath reveal_type(UPath.home()) # N: Revealed type is "upath.core.UPath" - case: upath_absolute disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").absolute()) # N: Revealed type is "upath.core.UPath" - case: upath_expanduser disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").expanduser()) # N: Revealed type is "upath.core.UPath" - case: upath_read_bytes disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").read_bytes()) # N: Revealed type is "builtins.bytes" - case: upath_read_text disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").read_text()) # N: Revealed type is "builtins.str" - case: upath_samefile disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").samefile("efg")) # N: Revealed type is "builtins.bool" - case: upath_write_bytes disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").write_bytes(b"efg")) # N: Revealed type is "builtins.int" - case: upath_write_text disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").write_text("efg")) # N: Revealed type is "builtins.int" - case: upath_link_to disable_cache: false main: | from upath import UPath UPath("abc").link_to # E: "UPath" has no attribute "link_to" [attr-defined] - case: upath_walk disable_cache: false main: | from upath import UPath reveal_type(UPath("abc").walk()) # N: Revealed type is "typing.Iterator[tuple[upath.core.UPath, builtins.list[builtins.str], builtins.list[builtins.str]]]" - case: upath_rename_extra_kwargs disable_cache: false main: | from upath import UPath UPath("abc").rename("efg") UPath("recursive bool").rename("efg", recursive=True) UPath("maxdepth int").rename("efg", maxdepth=1) UPath("untyped extras").rename("efg", overwrite=True, something="else") universal_pathlib-0.3.10/typesafety/test_upath_signatures.yml000066400000000000000000001037731514661127100246710ustar00rootroot00000000000000- case: upath_constructor_no_args disable_cache: false main: | from upath import UPath reveal_type(UPath()) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_string_arg disable_cache: false main: | from upath import UPath reveal_type(UPath(".")) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_purepath_arg disable_cache: false main: | from pathlib import PurePath from upath import UPath reveal_type(UPath(PurePath())) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_path_arg disable_cache: false main: | from pathlib import Path from upath import UPath reveal_type(UPath(Path())) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_pathlike_arg disable_cache: false main: | from upath import UPath class CustomPathLike: def __fspath__(self) -> str: return "" reveal_type(UPath(CustomPathLike())) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_vfspathlike_arg disable_cache: false main: | from upath import UPath class CustomVFSPathLike: def __vfspath__(self) -> str: return "" reveal_type(UPath(CustomVFSPathLike())) # N: Revealed type is "upath.core.UPath" - case: upath_constructor_upath_arg disable_cache: false main: | from upath import UPath reveal_type(UPath(UPath())) # N: Revealed type is "upath.core.UPath" - case: upath_implementations_constructor disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath - module: upath.implementations.cloud cls: S3Path - module: upath.implementations.cloud cls: GCSPath - module: upath.implementations.cloud cls: AzurePath - module: upath.implementations.cloud cls: HfPath - module: upath.implementations.data cls: DataPath - module: upath.implementations.ftp cls: FTPPath - module: upath.implementations.github cls: GitHubPath - module: upath.implementations.hdfs cls: HDFSPath - module: upath.implementations.http cls: HTTPPath - module: upath.implementations.local cls: PosixUPath - module: upath.implementations.local cls: WindowsUPath - module: upath.implementations.local cls: FilePath - module: upath.implementations.memory cls: MemoryPath - module: upath.implementations.sftp cls: SFTPPath - module: upath.implementations.smb cls: SMBPath - module: upath.implementations.tar cls: TarPath - module: upath.implementations.webdav cls: WebdavPath - module: upath.implementations.zip cls: ZipPath - module: upath.extensions cls: ProxyUPath main: | from pathlib import PurePath from {{ module }} import {{ cls }} reveal_type({{ cls }}()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type({{ cls }}(".")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type({{ cls }}(PurePath())) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type({{ cls }}({{ cls }}())) # N: Revealed type is "{{ module }}.{{ cls }}" - case: upath_attribute_parts disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.parts) # N: Revealed type is "typing.Sequence[builtins.str]" - case: upath_attribute_anchor disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.anchor) # N: Revealed type is "builtins.str" - case: upath_attribute_name disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.name) # N: Revealed type is "builtins.str" - case: upath_attribute_stem disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.stem) # N: Revealed type is "builtins.str" - case: upath_attribute_suffix disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.suffix) # N: Revealed type is "builtins.str" - case: upath_attribute_suffixes disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.suffixes) # N: Revealed type is "builtins.list[builtins.str]" - case: upath_attribute_root disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.root) # N: Revealed type is "builtins.str" - case: upath_attribute_drive disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.drive) # N: Revealed type is "builtins.str" - case: upath_attribute_parent disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.parent) # N: Revealed type is "upath.core.UPath" - case: upath_attribute_parents disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.parents) # N: Revealed type is "typing.Sequence[upath.core.UPath]" - case: upath_attribute_path disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.path) # N: Revealed type is "builtins.str" - case: upath_attribute_protocol disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.protocol) # N: Revealed type is "builtins.str" - case: upath_attribute_storage_options disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.storage_options) # N: Revealed type is "typing.Mapping[builtins.str, Any]" - case: upath_attribute__url disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p._url) # N: Revealed type is "tuple[builtins.str, builtins.str, builtins.str, builtins.str, builtins.str, fallback=urllib.parse.SplitResult]" # FIXME fsspec types are not available # - case: upath_attribute_fs # disable_cache: false # main: | # from upath import UPath # # p = UPath("") # reveal_type(p.fs) # N: Revealed type is "fsspec.spec.AbstractFileSystem" - case: upath_attribute_parser disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.parser) # N: Revealed type is "upath.types.UPathParser" - case: upath_attribute_info disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.info) # N: Revealed type is "upath.types._abc.PathInfo" - case: upath_subclass_attributes disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath - module: upath.implementations.cloud cls: S3Path - module: upath.implementations.cloud cls: GCSPath - module: upath.implementations.cloud cls: AzurePath - module: upath.implementations.cloud cls: HfPath - module: upath.implementations.data cls: DataPath - module: upath.implementations.ftp cls: FTPPath - module: upath.implementations.github cls: GitHubPath - module: upath.implementations.hdfs cls: HDFSPath - module: upath.implementations.http cls: HTTPPath - module: upath.implementations.local cls: PosixUPath - module: upath.implementations.local cls: WindowsUPath - module: upath.implementations.local cls: FilePath - module: upath.implementations.memory cls: MemoryPath - module: upath.implementations.sftp cls: SFTPPath - module: upath.implementations.smb cls: SMBPath - module: upath.implementations.tar cls: TarPath - module: upath.implementations.webdav cls: WebdavPath - module: upath.implementations.zip cls: ZipPath - module: upath.extensions cls: ProxyUPath main: | from {{ module }} import {{ cls }} p = {{ cls }}("") reveal_type(p.parts) # NR: Revealed type is "(typing\.Sequence|builtins\.tuple)\[builtins\.str(, ...)?\]" reveal_type(p.anchor) # N: Revealed type is "builtins.str" reveal_type(p.name) # N: Revealed type is "builtins.str" reveal_type(p.stem) # N: Revealed type is "builtins.str" reveal_type(p.suffix) # N: Revealed type is "builtins.str" reveal_type(p.suffixes) # N: Revealed type is "builtins.list[builtins.str]" reveal_type(p.root) # N: Revealed type is "builtins.str" reveal_type(p.drive) # N: Revealed type is "builtins.str" reveal_type(p.parent) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.parents) # N: Revealed type is "typing.Sequence[{{ module }}.{{ cls }}]" reveal_type(p.path) # N: Revealed type is "builtins.str" reveal_type(p.protocol) # N: Revealed type is "builtins.str" reveal_type(p.storage_options) # N: Revealed type is "typing.Mapping[builtins.str, Any]" # reveal_type(p.fs) # reveal_type(p.parser) reveal_type(p.info) # NR: Revealed type is ".*PathInfo" - case: upath_method_with_segments disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.with_segments("foo", "bar")) # N: Revealed type is "upath.core.UPath" - case: upath_method_vfspath disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.__vfspath__()) # N: Revealed type is "builtins.str" - case: upath_method_with_name disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.with_name("newname")) # N: Revealed type is "upath.core.UPath" - case: upath_method_with_stem disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.with_stem("newstem")) # N: Revealed type is "upath.core.UPath" - case: upath_method_with_suffix disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.with_suffix(".txt")) # N: Revealed type is "upath.core.UPath" - case: upath_method_joinpath disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.joinpath("foo")) # N: Revealed type is "upath.core.UPath" reveal_type(p.joinpath(UPath("bar"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_truediv disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p / "foo") # N: Revealed type is "upath.core.UPath" reveal_type(p / UPath("bar")) # N: Revealed type is "upath.core.UPath" - case: upath_method_rtruediv disable_cache: false main: | from upath import UPath p = UPath("") reveal_type("foo" / p) # N: Revealed type is "upath.core.UPath" - case: upath_method_full_match disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.full_match("*.txt")) # N: Revealed type is "builtins.bool" - case: upath_method_open_reader disable_cache: false main: | from upath import UPath from typing import BinaryIO p = UPath("") reveal_type(p.__open_reader__()) # N: Revealed type is "typing.BinaryIO" - case: upath_method_open_writer disable_cache: false main: | from upath import UPath from typing import BinaryIO p = UPath("") reveal_type(p.__open_writer__("a")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.__open_writer__("w")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.__open_writer__("x")) # N: Revealed type is "typing.BinaryIO" - case: upath_method_read_bytes disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.read_bytes()) # N: Revealed type is "builtins.bytes" - case: upath_method_read_text disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.read_text()) # N: Revealed type is "builtins.str" - case: upath_method_iterdir disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.iterdir()) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_method_glob disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.glob("*.txt")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_method_walk disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.walk()) # N: Revealed type is "typing.Iterator[tuple[upath.core.UPath, builtins.list[builtins.str], builtins.list[builtins.str]]]" - case: upath_method_readlink disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.readlink()) # N: Revealed type is "upath.core.UPath" - case: upath_method_copy disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.copy("target")) # N: Revealed type is "upath.core.UPath" reveal_type(p.copy(UPath("target"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_copy_into disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.copy_into("target_dir")) # N: Revealed type is "upath.core.UPath" reveal_type(p.copy_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_move disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.move("target")) # N: Revealed type is "upath.core.UPath" reveal_type(p.move(UPath("target"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_move_into disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.move_into("target_dir")) # N: Revealed type is "upath.core.UPath" reveal_type(p.move_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_symlink_to disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.symlink_to("target")) # N: Revealed type is "None" reveal_type(p.symlink_to(UPath("target"))) # N: Revealed type is "None" - case: upath_method_mkdir disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.mkdir()) # N: Revealed type is "None" - case: upath_method_write_bytes disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.write_bytes(b"data")) # N: Revealed type is "builtins.int" - case: upath_method_write_text disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.write_text("data")) # N: Revealed type is "builtins.int" - case: upath_method_copy_from disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p._copy_from(UPath("source"))) # N: Revealed type is "None" - case: upath_method_joinuri disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.joinuri("other")) # N: Revealed type is "upath.core.UPath" reveal_type(p.joinuri(UPath("other"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_str disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(str(p)) # N: Revealed type is "builtins.str" - case: upath_method_repr disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(repr(p)) # N: Revealed type is "builtins.str" - case: upath_method_open disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.open()) # N: Revealed type is "typing.TextIO" reveal_type(p.open("w")) # N: Revealed type is "typing.TextIO" reveal_type(p.open("rb")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.open("wb")) # N: Revealed type is "typing.BinaryIO" - case: upath_method_open_fsspec_kwargs disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath - module: upath.implementations.cloud cls: S3Path - module: upath.implementations.cloud cls: GCSPath - module: upath.implementations.cloud cls: AzurePath - module: upath.implementations.cloud cls: HfPath - module: upath.implementations.data cls: DataPath - module: upath.implementations.ftp cls: FTPPath - module: upath.implementations.github cls: GitHubPath - module: upath.implementations.hdfs cls: HDFSPath - module: upath.implementations.http cls: HTTPPath - module: upath.implementations.local cls: PosixUPath - module: upath.implementations.local cls: WindowsUPath - module: upath.implementations.local cls: FilePath - module: upath.implementations.memory cls: MemoryPath - module: upath.implementations.sftp cls: SFTPPath - module: upath.implementations.smb cls: SMBPath - module: upath.implementations.tar cls: TarPath - module: upath.implementations.webdav cls: WebdavPath - module: upath.implementations.zip cls: ZipPath - module: upath.extensions cls: ProxyUPath main: | from {{ module }} import {{ cls }} p = {{ cls }}("") reveal_type(p.open(mode="rb", compression="gz")) # N: Revealed type is "typing.BinaryIO" - case: upath_method_stat disable_cache: false main: | from upath import UPath import os p = UPath("") reveal_type(p.stat()) # N: Revealed type is "upath.types.StatResultType" - case: upath_method_lstat disable_cache: false main: | from upath import UPath import os p = UPath("") reveal_type(p.lstat()) # N: Revealed type is "upath.types.StatResultType" - case: upath_method_chmod disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.chmod(0o755)) # N: Revealed type is "None" - case: upath_method_exists disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.exists()) # N: Revealed type is "builtins.bool" - case: upath_method_is_dir disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_dir()) # N: Revealed type is "builtins.bool" - case: upath_method_is_file disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_file()) # N: Revealed type is "builtins.bool" - case: upath_method_is_mount disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_mount()) # N: Revealed type is "builtins.bool" - case: upath_method_is_symlink disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_symlink()) # N: Revealed type is "builtins.bool" - case: upath_method_is_junction disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_junction()) # N: Revealed type is "builtins.bool" - case: upath_method_is_block_device disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_block_device()) # N: Revealed type is "builtins.bool" - case: upath_method_is_char_device disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_char_device()) # N: Revealed type is "builtins.bool" - case: upath_method_is_fifo disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_fifo()) # N: Revealed type is "builtins.bool" - case: upath_method_is_socket disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_socket()) # N: Revealed type is "builtins.bool" - case: upath_method_is_reserved disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_reserved()) # N: Revealed type is "builtins.bool" - case: upath_method_expanduser disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.expanduser()) # N: Revealed type is "upath.core.UPath" - case: upath_method_rglob disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.rglob("*.txt")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" - case: upath_method_owner disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.owner()) # N: Revealed type is "builtins.str" - case: upath_method_group disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.group()) # N: Revealed type is "builtins.str" - case: upath_method_absolute disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.absolute()) # N: Revealed type is "upath.core.UPath" - case: upath_method_is_absolute disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_absolute()) # N: Revealed type is "builtins.bool" - case: upath_method_eq disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p == UPath("other")) # N: Revealed type is "builtins.bool" - case: upath_method_hash disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(hash(p)) # N: Revealed type is "builtins.int" - case: upath_method_lt disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p < UPath("other")) # N: Revealed type is "builtins.bool" - case: upath_method_le disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p <= UPath("other")) # N: Revealed type is "builtins.bool" - case: upath_method_gt disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p > UPath("other")) # N: Revealed type is "builtins.bool" - case: upath_method_ge disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p >= UPath("other")) # N: Revealed type is "builtins.bool" - case: upath_method_resolve disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.resolve()) # N: Revealed type is "upath.core.UPath" - case: upath_method_touch disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.touch()) # N: Revealed type is "None" - case: upath_method_lchmod disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.lchmod(0o755)) # N: Revealed type is "None" - case: upath_method_unlink disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.unlink()) # N: Revealed type is "None" - case: upath_method_rmdir disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.rmdir()) # N: Revealed type is "None" - case: upath_method_rename disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.rename("new_name")) # N: Revealed type is "upath.core.UPath" reveal_type(p.rename(UPath("new_name"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_replace disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.replace("target")) # N: Revealed type is "upath.core.UPath" reveal_type(p.replace(UPath("target"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_as_uri disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.as_uri()) # N: Revealed type is "builtins.str" - case: upath_method_as_posix disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.as_posix()) # N: Revealed type is "builtins.str" - case: upath_method_samefile disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.samefile("other")) # N: Revealed type is "builtins.bool" reveal_type(p.samefile(UPath("other"))) # N: Revealed type is "builtins.bool" - case: upath_classmethod_cwd disable_cache: false main: | from upath import UPath reveal_type(UPath.cwd()) # N: Revealed type is "upath.core.UPath" - case: upath_classmethod_home disable_cache: false main: | from upath import UPath reveal_type(UPath.home()) # N: Revealed type is "upath.core.UPath" - case: upath_classmethod_from_uri disable_cache: false main: | from upath import UPath reveal_type(UPath.from_uri("uri")) # N: Revealed type is "upath.core.UPath" - case: upath_method_relative_to disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.relative_to("other")) # N: Revealed type is "upath.core.UPath" reveal_type(p.relative_to(UPath("other"))) # N: Revealed type is "upath.core.UPath" - case: upath_method_is_relative_to disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.is_relative_to("other")) # N: Revealed type is "builtins.bool" reveal_type(p.is_relative_to(UPath("other"))) # N: Revealed type is "builtins.bool" - case: upath_method_hardlink_to disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.hardlink_to("target")) # N: Revealed type is "None" reveal_type(p.hardlink_to(UPath("target"))) # N: Revealed type is "None" - case: upath_method_match disable_cache: false main: | from upath import UPath p = UPath("") reveal_type(p.match("*.txt")) # N: Revealed type is "builtins.bool" - case: upath_subclass_methods disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath - module: upath.implementations.cloud cls: S3Path - module: upath.implementations.cloud cls: GCSPath - module: upath.implementations.cloud cls: AzurePath - module: upath.implementations.cloud cls: HfPath - module: upath.implementations.data cls: DataPath - module: upath.implementations.ftp cls: FTPPath - module: upath.implementations.github cls: GitHubPath - module: upath.implementations.hdfs cls: HDFSPath - module: upath.implementations.http cls: HTTPPath - module: upath.implementations.local cls: PosixUPath - module: upath.implementations.local cls: WindowsUPath - module: upath.implementations.local cls: FilePath - module: upath.implementations.memory cls: MemoryPath - module: upath.implementations.sftp cls: SFTPPath - module: upath.implementations.smb cls: SMBPath - module: upath.implementations.tar cls: TarPath - module: upath.implementations.webdav cls: WebdavPath - module: upath.implementations.zip cls: ZipPath - module: upath.extensions cls: ProxyUPath main: | from {{ module }} import {{ cls }} p = {{ cls }}("") # Path manipulation methods reveal_type(p.with_segments("foo", "bar")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.__vfspath__()) # N: Revealed type is "builtins.str" reveal_type(p.with_name("newname")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.with_stem("newstem")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.with_suffix(".txt")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.joinpath("foo")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.joinpath({{ cls }}("bar"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p / "foo") # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p / {{ cls }}("bar")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type("foo" / p) # N: Revealed type is "{{ module }}.{{ cls }}" # Boolean methods reveal_type(p.full_match("*.txt")) # N: Revealed type is "builtins.bool" reveal_type(p.exists()) # N: Revealed type is "builtins.bool" reveal_type(p.is_dir()) # N: Revealed type is "builtins.bool" reveal_type(p.is_file()) # N: Revealed type is "builtins.bool" reveal_type(p.is_mount()) # N: Revealed type is "builtins.bool" reveal_type(p.is_symlink()) # N: Revealed type is "builtins.bool" reveal_type(p.is_junction()) # N: Revealed type is "builtins.bool" reveal_type(p.is_block_device()) # N: Revealed type is "builtins.bool" reveal_type(p.is_char_device()) # N: Revealed type is "builtins.bool" reveal_type(p.is_fifo()) # N: Revealed type is "builtins.bool" reveal_type(p.is_socket()) # N: Revealed type is "builtins.bool" reveal_type(p.is_reserved()) # N: Revealed type is "builtins.bool" reveal_type(p.is_absolute()) # N: Revealed type is "builtins.bool" reveal_type(p.match("*.txt")) # N: Revealed type is "builtins.bool" reveal_type(p.is_relative_to("other")) # N: Revealed type is "builtins.bool" reveal_type(p.is_relative_to({{ cls }}("other"))) # N: Revealed type is "builtins.bool" # File I/O methods reveal_type(p.__open_reader__()) # N: Revealed type is "typing.BinaryIO" reveal_type(p.__open_writer__("a")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.__open_writer__("w")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.__open_writer__("x")) # N: Revealed type is "typing.BinaryIO" reveal_type(p.read_bytes()) # N: Revealed type is "builtins.bytes" reveal_type(p.read_text()) # N: Revealed type is "builtins.str" reveal_type(p.open()) # NR: Revealed type is ".*TextIO.*" reveal_type(p.open("w")) # NR: Revealed type is ".*TextIO.*" reveal_type(p.open("rb")) # NR: Revealed type is ".*(BinaryIO|BufferedReader).*" reveal_type(p.open("wb")) # NR: Revealed type is ".*(BinaryIO|BufferedWriter).*" reveal_type(p.write_bytes(b"data")) # N: Revealed type is "builtins.int" reveal_type(p.write_text("data")) # N: Revealed type is "builtins.int" # Iterator methods reveal_type(p.iterdir()) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" reveal_type(p.glob("*.txt")) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" reveal_type(p.rglob("*.txt")) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" reveal_type(p.walk()) # N: Revealed type is "typing.Iterator[tuple[{{ module }}.{{ cls }}, builtins.list[builtins.str], builtins.list[builtins.str]]]" # Path resolution methods reveal_type(p.readlink()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.expanduser()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.absolute()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.resolve()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.relative_to("other")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.relative_to({{ cls }}("other"))) # N: Revealed type is "{{ module }}.{{ cls }}" # File system operations that return paths reveal_type(p.copy("target")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.copy({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.copy_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.copy_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.move("target")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.move({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.move_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.move_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.rename("new_name")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.rename({{ cls }}("new_name"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.replace("target")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.replace({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.joinuri("other")) # NR: Revealed type is "(upath.core.UPath|upath.extensions.ProxyUPath)" reveal_type(p.joinuri({{ cls }}("other"))) # NR: Revealed type is "(upath.core.UPath|upath.extensions.ProxyUPath)" # String returning methods reveal_type(str(p)) # N: Revealed type is "builtins.str" reveal_type(repr(p)) # N: Revealed type is "builtins.str" reveal_type(p.as_uri()) # N: Revealed type is "builtins.str" reveal_type(p.as_posix()) # N: Revealed type is "builtins.str" reveal_type(p.owner()) # N: Revealed type is "builtins.str" reveal_type(p.group()) # N: Revealed type is "builtins.str" # Stat methods reveal_type(p.stat()) # NR: Revealed type is ".*(StatResultType|stat_result).*" reveal_type(p.lstat()) # NR: Revealed type is ".*(StatResultType|stat_result).*" # None methods reveal_type(p.symlink_to("target")) # N: Revealed type is "None" reveal_type(p.symlink_to({{ cls }}("target"))) # N: Revealed type is "None" reveal_type(p.mkdir()) # N: Revealed type is "None" reveal_type(p._copy_from({{ cls }}("source"))) # N: Revealed type is "None" reveal_type(p.chmod(0o755)) # N: Revealed type is "None" reveal_type(p.touch()) # N: Revealed type is "None" reveal_type(p.lchmod(0o755)) # N: Revealed type is "None" reveal_type(p.unlink()) # N: Revealed type is "None" reveal_type(p.rmdir()) # N: Revealed type is "None" reveal_type(p.hardlink_to("target")) # N: Revealed type is "None" reveal_type(p.hardlink_to({{ cls }}("target"))) # N: Revealed type is "None" # Comparison methods reveal_type(p == {{ cls }}("other")) # N: Revealed type is "builtins.bool" reveal_type(hash(p)) # N: Revealed type is "builtins.int" reveal_type(p < {{ cls }}("other")) # N: Revealed type is "builtins.bool" reveal_type(p <= {{ cls }}("other")) # N: Revealed type is "builtins.bool" reveal_type(p > {{ cls }}("other")) # N: Revealed type is "builtins.bool" reveal_type(p >= {{ cls }}("other")) # N: Revealed type is "builtins.bool" reveal_type(p.samefile("other")) # N: Revealed type is "builtins.bool" reveal_type(p.samefile({{ cls }}("other"))) # N: Revealed type is "builtins.bool" # Class methods reveal_type({{ cls }}.cwd()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type({{ cls }}.home()) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type({{ cls }}.from_uri("uri")) # N: Revealed type is "{{ module }}.{{ cls }}" universal_pathlib-0.3.10/typesafety/test_upath_types.yml000066400000000000000000000342051514661127100236420ustar00rootroot00000000000000 - case: upath_types_joinablepath_upath disable_cache: false main: | from upath import UPath from upath.types import JoinablePath a: JoinablePath = UPath() - case: upath_types_joinablepathlike disable_cache: false main: | from pathlib import PurePath from pathlib import Path from upath import UPath from upath.types import JoinablePathLike a: JoinablePathLike = "abc" b: JoinablePathLike = PurePath() c: JoinablePathLike = Path() d: JoinablePathLike = UPath() - case: get_upath_class_fsspec_protocols disable_cache: false parametrized: - cls_fqn: upath.implementations.cached.SimpleCachePath protocol: simplecache - cls_fqn: upath.implementations.cloud.S3Path protocol: s3 - cls_fqn: upath.implementations.cloud.GCSPath protocol: gcs - cls_fqn: upath.implementations.cloud.AzurePath protocol: abfs - cls_fqn: upath.implementations.cloud.HfPath protocol: hf - cls_fqn: upath.implementations.data.DataPath protocol: data - cls_fqn: upath.implementations.ftp.FTPPath protocol: ftp - cls_fqn: upath.implementations.github.GitHubPath protocol: github - cls_fqn: upath.implementations.hdfs.HDFSPath protocol: hdfs - cls_fqn: upath.implementations.http.HTTPPath protocol: http - cls_fqn: upath.implementations.local.FilePath protocol: file - cls_fqn: upath.implementations.memory.MemoryPath protocol: memory - cls_fqn: upath.implementations.sftp.SFTPPath protocol: sftp - cls_fqn: upath.implementations.smb.SMBPath protocol: smb - cls_fqn: upath.implementations.tar.TarPath protocol: tar - cls_fqn: upath.implementations.webdav.WebdavPath protocol: webdav - cls_fqn: upath.implementations.zip.ZipPath protocol: zip main: | from upath.registry import get_upath_class path_cls = get_upath_class("{{ protocol }}") reveal_type(path_cls) # N: Revealed type is "type[{{ cls_fqn }}]" - case: get_upath_class_pathlib_unix disable_cache: false skip: sys.platform == "win32" main: | from upath.registry import get_upath_class path_cls = get_upath_class("") reveal_type(path_cls) # N: Revealed type is "type[upath.implementations.local.PosixUPath]" - case: get_upath_class_pathlib_win disable_cache: false skip: sys.platform != "win32" main: | from upath.registry import get_upath_class path_cls = get_upath_class("") reveal_type(path_cls) # N: Revealed type is "type[upath.implementations.local.WindowsUPath]" - case: get_upath_class_unknown_protocol_py39 disable_cache: false skip: sys.version_info >= (3, 10) main: | from upath.registry import get_upath_class path_cls = get_upath_class("some-unknown-protocol") reveal_type(path_cls) # N: Revealed type is "Union[type[upath.core.UPath], None]" - case: get_upath_class_unknown_protocol_py310plus disable_cache: false skip: sys.version_info < (3, 10) main: | from upath.registry import get_upath_class path_cls = get_upath_class("some-unknown-protocol") reveal_type(path_cls) # N: Revealed type is "type[upath.core.UPath] | None" - case: upath__new__fsspec_protocols disable_cache: false parametrized: - cls_fqn: upath.implementations.cached.SimpleCachePath protocol: simplecache - cls_fqn: upath.implementations.cloud.S3Path protocol: s3 - cls_fqn: upath.implementations.cloud.GCSPath protocol: gcs - cls_fqn: upath.implementations.cloud.AzurePath protocol: abfs - cls_fqn: upath.implementations.cloud.HfPath protocol: hf - cls_fqn: upath.implementations.data.DataPath protocol: data - cls_fqn: upath.implementations.ftp.FTPPath protocol: ftp - cls_fqn: upath.implementations.github.GitHubPath protocol: github - cls_fqn: upath.implementations.hdfs.HDFSPath protocol: hdfs - cls_fqn: upath.implementations.http.HTTPPath protocol: http - cls_fqn: upath.implementations.local.FilePath protocol: file - cls_fqn: upath.implementations.memory.MemoryPath protocol: memory - cls_fqn: upath.implementations.sftp.SFTPPath protocol: sftp - cls_fqn: upath.implementations.smb.SMBPath protocol: smb - cls_fqn: upath.implementations.tar.TarPath protocol: tar - cls_fqn: upath.implementations.webdav.WebdavPath protocol: webdav - cls_fqn: upath.implementations.zip.ZipPath protocol: zip - cls_fqn: upath.core.UPath protocol: unknown-protocol main: | import upath p = upath.UPath(".", protocol="{{ protocol }}") reveal_type(p) # N: Revealed type is "{{ cls_fqn }}" - case: upath__new__empty_protocol disable_cache: false skip: sys.platform == "win32" main: | from upath.core import UPath p = UPath("asd", protocol="") reveal_type(p) # N: Revealed type is "upath.implementations.local.PosixUPath" - case: get_upath_class_pathlib_win disable_cache: false skip: sys.platform != "win32" main: | from upath.core import UPath p = UPath("", protocol="") reveal_type(p) # N: Revealed type is "upath.implementations.local.WindowsUPath" - case: upath_constructor_sopts_correct_type disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath supported_example_name: check_files supported_example_value: True - module: upath.implementations.cloud cls: GCSPath supported_example_name: project supported_example_value: '"my-project"' - module: upath.implementations.cloud cls: S3Path supported_example_name: anon supported_example_value: True - module: upath.implementations.cloud cls: AzurePath supported_example_name: account_name supported_example_value: '"myaccount"' - module: upath.implementations.cloud cls: HfPath supported_example_name: token supported_example_value: '"abcd1234"' - module: upath.implementations.data cls: DataPath supported_example_name: use_listings_cache supported_example_value: False - module: upath.implementations.ftp cls: FTPPath supported_example_name: host supported_example_value: '"ftp.example.com"' - module: upath.implementations.github cls: GitHubPath supported_example_name: org supported_example_value: '"fsspec"' - module: upath.implementations.hdfs cls: HDFSPath supported_example_name: host supported_example_value: '"localhost"' - module: upath.implementations.http cls: HTTPPath supported_example_name: simple_links supported_example_value: True - module: upath.implementations.local cls: FilePath supported_example_name: auto_mkdir supported_example_value: True - module: upath.implementations.memory cls: MemoryPath supported_example_name: use_listings_cache supported_example_value: True - module: upath.implementations.sftp cls: SFTPPath supported_example_name: host supported_example_value: '"sftp.example.com"' - module: upath.implementations.smb cls: SMBPath supported_example_name: timeout supported_example_value: 60 - module: upath.implementations.tar cls: TarPath supported_example_name: compression supported_example_value: '"gzip"' - module: upath.implementations.webdav cls: WebdavPath supported_example_name: base_url supported_example_value: '"https://webdav.example.com"' - module: upath.implementations.zip cls: ZipPath supported_example_name: compression supported_example_value: 9 main: | from {{ module }} import {{ cls }} p = {{ cls }}(".", {{ supported_example_name }}={{ supported_example_value }}) reveal_type(p) # N: Revealed type is "{{ module }}.{{ cls }}" - case: upath_constructor_sopts_incorrect_type disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath supported_example_name: check_files unsupported_example_value: '"blub"' - module: upath.implementations.cloud cls: GCSPath supported_example_name: version_aware unsupported_example_value: '"yes"' - module: upath.implementations.cloud cls: S3Path supported_example_name: anon unsupported_example_value: '"blah"' - module: upath.implementations.cloud cls: AzurePath supported_example_name: account_name unsupported_example_value: '123' - module: upath.implementations.cloud cls: HfPath supported_example_name: endpoint unsupported_example_value: '123' - module: upath.implementations.data cls: DataPath supported_example_name: use_listings_cache unsupported_example_value: '"blub"' - module: upath.implementations.ftp cls: FTPPath supported_example_name: host unsupported_example_value: '123' - module: upath.implementations.github cls: GitHubPath supported_example_name: repo unsupported_example_value: 'True' - module: upath.implementations.hdfs cls: HDFSPath supported_example_name: host unsupported_example_value: '8020' - module: upath.implementations.http cls: HTTPPath supported_example_name: simple_links unsupported_example_value: '"no"' - module: upath.implementations.local cls: FilePath supported_example_name: auto_mkdir unsupported_example_value: '"abc"' - module: upath.implementations.memory cls: MemoryPath supported_example_name: use_listings_cache unsupported_example_value: '{}' - module: upath.implementations.sftp cls: SFTPPath supported_example_name: host unsupported_example_value: '[]' - module: upath.implementations.smb cls: SMBPath supported_example_name: host unsupported_example_value: 'False' - module: upath.implementations.tar cls: TarPath supported_example_name: compression unsupported_example_value: '[]' - module: upath.implementations.webdav cls: WebdavPath supported_example_name: base_url unsupported_example_value: '123' - module: upath.implementations.zip cls: ZipPath supported_example_name: compression unsupported_example_value: '"hello"' main: | from {{ module }} import {{ cls }} p = {{ cls }}(".", {{ supported_example_name }}={{ unsupported_example_value }}) # ER: Argument "{{ supported_example_name }}" to "{{ cls }}" has incompatible type.* - case: upath_constructor_sopts_via_dict disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath - module: upath.implementations.cloud cls: GCSPath - module: upath.implementations.cloud cls: S3Path - module: upath.implementations.cloud cls: AzurePath - module: upath.implementations.cloud cls: HfPath - module: upath.implementations.data cls: DataPath - module: upath.implementations.ftp cls: FTPPath - module: upath.implementations.github cls: GitHubPath - module: upath.implementations.hdfs cls: HDFSPath - module: upath.implementations.http cls: HTTPPath - module: upath.implementations.local cls: FilePath - module: upath.implementations.memory cls: MemoryPath - module: upath.implementations.sftp cls: SFTPPath - module: upath.implementations.smb cls: SMBPath - module: upath.implementations.tar cls: TarPath - module: upath.implementations.webdav cls: WebdavPath - module: upath.implementations.zip cls: ZipPath main: | from typing import Any from {{ module }} import {{ cls }} kw: dict[str, Any] = {} p = {{ cls }}(".", **kw) reveal_type(p) # N: Revealed type is "{{ module }}.{{ cls }}" - case: upath_constructor_sopts_via_typed_dict disable_cache: false parametrized: - module: upath.implementations.cached cls: SimpleCachePath td: SimpleCacheStorageOptions - module: upath.implementations.cloud cls: GCSPath td: GCSStorageOptions - module: upath.implementations.cloud cls: S3Path td: S3StorageOptions - module: upath.implementations.cloud cls: AzurePath td: AzureStorageOptions - module: upath.implementations.cloud cls: HfPath td: HfStorageOptions - module: upath.implementations.data cls: DataPath td: DataStorageOptions - module: upath.implementations.ftp cls: FTPPath td: FTPStorageOptions - module: upath.implementations.github cls: GitHubPath td: GitHubStorageOptions - module: upath.implementations.hdfs cls: HDFSPath td: HDFSStorageOptions - module: upath.implementations.http cls: HTTPPath td: HTTPStorageOptions - module: upath.implementations.local cls: FilePath td: FileStorageOptions - module: upath.implementations.memory cls: MemoryPath td: MemoryStorageOptions - module: upath.implementations.sftp cls: SFTPPath td: SFTPStorageOptions - module: upath.implementations.smb cls: SMBPath td: SMBStorageOptions - module: upath.implementations.tar cls: TarPath td: TarStorageOptions - module: upath.implementations.webdav cls: WebdavPath td: WebdavStorageOptions - module: upath.implementations.zip cls: ZipPath td: ZipStorageOptions main: | from {{ module }} import {{ cls }} from upath.types.storage_options import {{ td }} kw: {{ td }} = {{ td }}() p = {{ cls }}(".", **kw) reveal_type(p) # N: Revealed type is "{{ module }}.{{ cls }}" # FIXME: mypy emits a 'defined here' note when emitting the error below. # seems to be a limitation/bug in pytest-mypy-plugins that I can't match it # # - case: upath_constructor_sopts_incorrect_name # disable_cache: false # regex: yes # parametrized: # - module: upath.implementations.cached # cls: SimpleCachePath # unsupported_example_name: idontexist # main: | # from {{ module }} import {{ cls }} # # p = {{ cls }}(".", {{ unsupported_example_name }}=1) # out: | # \\.\\..* # main:3: error: Unexpected keyword argument "idontexist" to "{{ cls }}".* universal_pathlib-0.3.10/upath/000077500000000000000000000000001514661127100164335ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/__init__.py000066400000000000000000000014301514661127100205420ustar00rootroot00000000000000"""Pathlib API extended to use fsspec backends.""" from __future__ import annotations from typing import TYPE_CHECKING try: from upath._version import __version__ except ImportError: __version__ = "not-installed" if TYPE_CHECKING: from upath.core import UnsupportedOperation from upath.core import UPath __all__ = ["UPath", "UnsupportedOperation"] def __getattr__(name): if name == "UPath": from upath.core import UPath globals()["UPath"] = UPath return UPath elif name == "UnsupportedOperation": from upath.core import UnsupportedOperation globals()["UnsupportedOperation"] = UnsupportedOperation return UnsupportedOperation else: raise AttributeError(f"module {__name__} has no attribute {name}") universal_pathlib-0.3.10/upath/_chain.py000066400000000000000000000304201514661127100202250ustar00rootroot00000000000000from __future__ import annotations import sys import warnings from collections import defaultdict from collections import deque from collections.abc import Iterator from collections.abc import MutableMapping from collections.abc import Sequence from collections.abc import Set from itertools import zip_longest from typing import TYPE_CHECKING from typing import Any from typing import NamedTuple from upath._flavour import WrappedFileSystemFlavour from upath._protocol import get_upath_protocol from upath.registry import available_implementations from upath.types import UNSET_DEFAULT if TYPE_CHECKING: if sys.version_info >= (3, 11): from typing import Never from typing import Self else: from typing_extensions import Never from typing_extensions import Self __all__ = [ "ChainSegment", "Chain", "FSSpecChainParser", "DEFAULT_CHAIN_PARSER", ] class ChainSegment(NamedTuple): path: str | None # support for path passthrough (i.e. simplecache) protocol: str storage_options: dict[str, Any] class Chain: """holds current chain segments""" __slots__ = ( "_segments", "_index", ) def __init__( self, *segments: ChainSegment, index: int = 0, ) -> None: if not (0 <= index < len(segments)): raise ValueError("index must be between 0 and len(segments)") self._segments = segments self._index = index def __repr__(self) -> str: args = ", ".join(map(repr, self._segments)) if self._index != 0: args += f", index={self._index}" return f"{type(self).__name__}({args})" @property def current(self) -> ChainSegment: return self._segments[self._index] @property def _path_index(self) -> int: for idx, segment in enumerate(self._segments[self._index :], start=self._index): if segment.path is not None: return idx raise IndexError("No target path found") @property def active_path(self) -> str: path = self._segments[self._path_index].path if path is None: raise RuntimeError return path @property def active_path_protocol(self) -> str: return self._segments[self._path_index].protocol def replace( self, *, path: str | None = None, protocol: str | None = None, storage_options: dict[str, Any] | None = None, ) -> Self: """replace the current chain segment keeping remaining chain segments""" segments = self.to_list() index = self._index replacements: MutableMapping[int, dict[str, Any]] = defaultdict(dict) if protocol is not None: replacements[index]["protocol"] = protocol if storage_options is not None: replacements[index]["storage_options"] = storage_options if path is not None: replacements[self._path_index]["path"] = path for idx, items in replacements.items(): segments[idx] = segments[idx]._replace(**items) return type(self)(*segments, index=index) def to_list(self) -> list[ChainSegment]: """return a list of chain segments unnesting target_* segments""" queue = deque(self._segments) segments = [] while queue: segment = queue.popleft() if ( "target_protocol" in segment.storage_options and "fo" in segment.storage_options ): storage_options = segment.storage_options.copy() target_options = storage_options.pop("target_options", {}) target_protocol = storage_options.pop("target_protocol") fo = storage_options.pop("fo") queue.appendleft(ChainSegment(fo, target_protocol, target_options)) segments.append( ChainSegment(segment.path, segment.protocol, storage_options) ) elif not segments or segment != segments[-1]: segments.append(segment) return segments @classmethod def from_list(cls, segments: list[ChainSegment], index: int = 0) -> Self: return cls(*segments, index=index) def nest(self) -> ChainSegment: """return a nested target_* structure""" # see: fsspec.core.url_to_fs inkwargs: dict[str, Any] = {} # Reverse iterate the chain, creating a nested target_* structure chain = self._segments _prev = chain[-1].path for i, ch in enumerate(reversed(chain)): urls, protocol, kw = ch if urls is None: urls = _prev _prev = urls if i == len(chain) - 1: inkwargs = {**kw, **inkwargs} continue inkwargs["target_options"] = {**kw, **inkwargs} inkwargs["target_protocol"] = protocol inkwargs["fo"] = urls # codespell:ignore fo urlpath, protocol, _ = chain[0] return ChainSegment(urlpath, protocol, inkwargs) def _iter_fileobject_protocol_options( fileobject: str | None, protocol: str, storage_options: dict[str, Any], /, ) -> Iterator[tuple[str | None, str, dict[str, Any]]]: """yields fileobject, protocol and remaining storage options""" so = storage_options.copy() while "target_protocol" in so: t_protocol = so.pop("target_protocol", "") t_fileobject = so.pop("fo", None) # codespell:ignore fo t_so = so.pop("target_options", {}) yield fileobject, protocol, so fileobject, protocol, so = t_fileobject, t_protocol, t_so yield fileobject, protocol, so class FSSpecChainParser: """parse an fsspec chained urlpath""" def __init__(self) -> None: self.link: str = "::" self.known_protocols: Set[str] = set() def unchain( self, path: str, _deprecated_storage_options: Never = UNSET_DEFAULT, /, *, protocol: str | None = None, storage_options: dict[str, Any] | None = None, ) -> list[ChainSegment]: """implements same behavior as fsspec.core._un_chain two differences: 1. it sets the urlpath to None for upstream filesystems that passthrough 2. it checks against the known protocols for exact matches """ if _deprecated_storage_options is not UNSET_DEFAULT: warnings.warn( "passing storage_options as positional argument is deprecated, " "pass as keyword argument instead", DeprecationWarning, stacklevel=2, ) if storage_options is not None: raise ValueError( "cannot pass storage_options both positionally and as keyword" ) storage_options = _deprecated_storage_options protocol = protocol or storage_options.get("protocol") if storage_options is None: storage_options = {} segments: list[ChainSegment] = [] path_bit: str | None next_path_overwrite: str | None = None for proto0, bit in zip_longest([protocol], path.split(self.link)): # get protocol and path_bit if ( "://" in bit # uri-like, fast-path (redundant) or "/" in bit # path-like, fast-path ): proto = get_upath_protocol(bit, protocol=proto0) flavour = WrappedFileSystemFlavour.from_protocol(proto) path_bit = flavour.strip_protocol(bit) extra_so = flavour.get_kwargs_from_url(bit) elif bit in self.known_protocols and ( proto0 is None or bit == proto0 ): # exact match a fsspec protocol proto = bit path_bit = None extra_so = {} elif bit in (m := set(available_implementations(fallback=True))) and ( proto0 is None or bit == proto0 ): self.known_protocols = m proto = bit path_bit = None extra_so = {} else: proto = get_upath_protocol(bit, protocol=proto0) flavour = WrappedFileSystemFlavour.from_protocol(proto) path_bit = flavour.strip_protocol(bit) extra_so = flavour.get_kwargs_from_url(bit) if proto in {"blockcache", "filecache", "simplecache"}: if path_bit is not None: next_path_overwrite = path_bit or "/" path_bit = None elif next_path_overwrite is not None: path_bit = next_path_overwrite next_path_overwrite = None segments.append(ChainSegment(path_bit, proto, extra_so)) root_so = segments[0].storage_options for segment, proto_fo_so in zip_longest( segments, _iter_fileobject_protocol_options( path_bit if segments else None, protocol or "", storage_options, ), ): t_fo, t_proto, t_so = proto_fo_so or (None, "", {}) if segment is None: if next_path_overwrite is not None: t_fo = next_path_overwrite next_path_overwrite = None segments.append(ChainSegment(t_fo, t_proto, t_so)) else: proto = segment.protocol # check if protocol is consistent with storage options if t_proto and t_proto != proto: raise ValueError( f"protocol {proto!r} collides with target_protocol {t_proto!r}" ) # update the storage_options segment.storage_options.update(root_so.pop(proto, {})) segment.storage_options.update(t_so) return segments def chain(self, segments: Sequence[ChainSegment]) -> tuple[str, dict[str, Any]]: """returns a chained urlpath from the segments""" urlpaths = [] kwargs = {} for segment in segments: if segment.protocol and segment.path is not None: # FIXME: currently unstrip_protocol is only implemented by # AbstractFileSystem, LocalFileSystem, and OSSFileSystem # so to make this work we just implement it ourselves here. # To do this properly we would need to instantiate the # filesystem with its storage options and call # fs.unstrip_protocol(segment.path) if segment.path.startswith(f"{segment.protocol}:/"): urlpath = segment.path else: urlpath = f"{segment.protocol}://{segment.path}" elif segment.protocol: urlpath = segment.protocol elif segment.path is not None: urlpath = segment.path else: warnings.warn( f"skipping invalid segment {segment}", RuntimeWarning, stacklevel=2, ) continue urlpaths.append(urlpath) # TODO: ensure roundtrip with unchain behavior if segment.storage_options: kwargs[segment.protocol] = segment.storage_options return self.link.join(urlpaths), kwargs DEFAULT_CHAIN_PARSER = FSSpecChainParser() if __name__ == "__main__": from pprint import pp from fsspec.core import _un_chain chained_path = "simplecache::zip://haha.csv::gcs://bucket/file.zip" chained_kw = {"zip": {"allowZip64": False}} print(chained_path, chained_kw) out0 = _un_chain(chained_path, chained_kw) out1 = FSSpecChainParser().unchain(chained_path, storage_options=chained_kw) pp(out0) pp(out1) rechained_path, rechained_kw = FSSpecChainParser().chain(out1) print(rechained_path, rechained_kw) # UPath should store segments and access the path to operate on # through segments.current.path segments0 = Chain.from_list(segments=out1, index=1) assert segments0.current.protocol == "zip" # try to switch out zip path segments1 = segments0.replace(path="/newfile.csv") new_path, new_kw = FSSpecChainParser().chain(segments1.to_list()) print(new_path, new_kw) universal_pathlib-0.3.10/upath/_flavour.py000066400000000000000000000401011514661127100206160ustar00rootroot00000000000000from __future__ import annotations import os.path import posixpath import sys import warnings from collections.abc import Mapping from functools import lru_cache from typing import TYPE_CHECKING from typing import Any from typing import TypedDict from urllib.parse import SplitResult from urllib.parse import urlsplit from fsspec.registry import known_implementations from fsspec.registry import registry as _class_registry from fsspec.spec import AbstractFileSystem import upath from upath._flavour_sources import FileSystemFlavourBase from upath._flavour_sources import flavour_registry from upath._protocol import get_upath_protocol from upath._protocol import normalize_empty_netloc from upath.types import JoinablePathLike from upath.types import UPathParser if TYPE_CHECKING: if sys.version_info >= (3, 12): from typing import TypeAlias else: TypeAlias = Any from upath.core import UPath __all__ = [ "LazyFlavourDescriptor", "default_flavour", "upath_urijoin", "upath_get_kwargs_from_url", "upath_strip_protocol", ] class_registry: Mapping[str, type[AbstractFileSystem]] = _class_registry class AnyProtocolFileSystemFlavour(FileSystemFlavourBase): sep = "/" protocol = () root_marker = "/" @classmethod def _strip_protocol(cls, path: str) -> str: protocol = get_upath_protocol(path) if path.startswith(protocol + "://"): path = path[len(protocol) + 3 :] elif path.startswith(protocol + "::"): path = path[len(protocol) + 2 :] path = path.rstrip("/") return path or cls.root_marker @staticmethod def _get_kwargs_from_urls(path: str) -> dict[str, Any]: return {} @classmethod def _parent(cls, path): path = cls._strip_protocol(path) if "/" in path: parent = path.rsplit("/", 1)[0].lstrip(cls.root_marker) return cls.root_marker + parent else: return cls.root_marker class ProtocolConfig(TypedDict): netloc_is_anchor: set[str] supports_empty_parts: set[str] meaningful_trailing_slash: set[str] root_marker_override: dict[str, str] class WrappedFileSystemFlavour(UPathParser): # (pathlib_abc.FlavourBase) """flavour class for universal_pathlib **INTERNAL AND VERY MUCH EXPERIMENTAL** Implements the fsspec compatible low-level lexical operations on PurePathBase-like objects. Note: In case you find yourself in need of subclassing this class, please open an issue in the universal_pathlib issue tracker: https://github.com/fsspec/universal_pathlib/issues Ideally we can find a way to make your use-case work by adding more functionality to this class. """ # Note: # It would be ideal if there would be a way to avoid the need for # indicating the following settings via the protocol. This is a # workaround to be able to implement the flavour correctly. # TODO: # These settings should be configured on the UPath class?!? protocol_config: ProtocolConfig = { "netloc_is_anchor": { "http", "https", "s3", "s3a", "smb", "gs", "gcs", "az", "adl", "abfs", "abfss", }, "supports_empty_parts": { "http", "https", "s3", "s3a", "gs", "gcs", "az", "adl", "abfs", }, "meaningful_trailing_slash": { "http", "https", }, "root_marker_override": { "smb": "/", "ssh": "/", "sftp": "/", }, } def __init__( self, spec: type[AbstractFileSystem | FileSystemFlavourBase] | AbstractFileSystem, *, netloc_is_anchor: bool = False, supports_empty_parts: bool = False, meaningful_trailing_slash: bool = False, root_marker_override: str | None = None, ) -> None: """initialize the flavour with the given fsspec""" self._spec = spec # netloc is considered an anchor, influences: # - splitdrive # - join self.netloc_is_anchor = bool(netloc_is_anchor) # supports empty parts, influences: # - join # - UPath._parse_path self.supports_empty_parts = bool(supports_empty_parts) # meaningful trailing slash, influences: # - join # - UPath._parse_path self.has_meaningful_trailing_slash = bool(meaningful_trailing_slash) # some filesystems require UPath to enforce a specific root marker if root_marker_override is None: self.root_marker_override = None else: self.root_marker_override = str(root_marker_override) @classmethod @lru_cache(maxsize=None) def from_protocol( cls, protocol: str, ) -> WrappedFileSystemFlavour: """return the fsspec flavour for the given protocol""" _c = cls.protocol_config config: dict[str, Any] = { "netloc_is_anchor": protocol in _c["netloc_is_anchor"], "supports_empty_parts": protocol in _c["supports_empty_parts"], "meaningful_trailing_slash": protocol in _c["meaningful_trailing_slash"], "root_marker_override": _c["root_marker_override"].get(protocol), } # first try to get an already imported fsspec filesystem class try: return cls(class_registry[protocol], **config) except KeyError: pass # next try to get the flavour from the generated flavour registry # to avoid imports try: return cls(flavour_registry[protocol], **config) except KeyError: pass # finally fallback to a default flavour for the protocol if protocol in known_implementations: warnings.warn( f"Could not find default for known protocol {protocol!r}." " Creating a default flavour for it. Please report this" " to the universal_pathlib issue tracker.", UserWarning, stacklevel=2, ) return cls(AnyProtocolFileSystemFlavour, **config) def __repr__(self): if isinstance(self._spec, type): return f"" else: return f"" # === fsspec.AbstractFileSystem =================================== @property def protocol(self) -> tuple[str, ...]: if isinstance(self._spec.protocol, str): return (self._spec.protocol,) else: return self._spec.protocol @property def root_marker(self) -> str: if self.root_marker_override is not None: return self.root_marker_override else: return self._spec.root_marker @property def local_file(self) -> bool: return bool(getattr(self._spec, "local_file", False)) @staticmethod def stringify_path(pth: JoinablePathLike) -> str: if isinstance(pth, str): out = pth elif isinstance(pth, upath.UPath) and not pth.is_absolute(): out = str(pth) elif getattr(pth, "__fspath__", None) is not None: assert hasattr(pth, "__fspath__") out = pth.__fspath__() elif isinstance(pth, os.PathLike): out = str(pth) elif isinstance(pth, upath.UPath) and pth.is_absolute(): out = pth.path else: out = str(pth) return normalize_empty_netloc(out) def strip_protocol(self, pth: JoinablePathLike) -> str: pth = self.stringify_path(pth) return self._spec._strip_protocol(pth) or self.root_marker def get_kwargs_from_url(self, url: JoinablePathLike) -> dict[str, Any]: # NOTE: the public variant is _from_url not _from_urls if hasattr(url, "storage_options"): return dict(url.storage_options) url = self.stringify_path(url) return self._spec._get_kwargs_from_urls(url) def parent(self, path: JoinablePathLike) -> str: path = self.stringify_path(path) return self._spec._parent(path) # === pathlib_abc.FlavourBase ===================================== @property def sep(self) -> str: # type: ignore[override] return self._spec.sep @property def altsep(self) -> str | None: # type: ignore[override] return getattr(self._spec, "altsep", None) def isabs(self, path: JoinablePathLike) -> bool: path = self.strip_protocol(path) if self.local_file: return os.path.isabs(path) else: return path.startswith(self.root_marker) def join(self, path: JoinablePathLike, *paths: JoinablePathLike) -> str: if not paths: return self.strip_protocol(path) or self.root_marker if self.local_file: p = os.path.join( self.strip_protocol(path), *map(self.stringify_path, paths), ) return p if os.name != "nt" else p.replace("\\", "/") if self.netloc_is_anchor: drv, p0 = self.splitdrive(path) pN = list(map(self.stringify_path, paths)) if not drv and not p0: path, *pN = pN drv, p0 = self.splitdrive(path) p0 = p0 or self.sep else: p0 = str(self.strip_protocol(path)) or self.root_marker pN = list(map(self.stringify_path, paths)) drv = "" if self.supports_empty_parts: return drv + self.sep.join([p0.removesuffix(self.sep), *pN]) else: return drv + posixpath.join(p0, *pN) def split(self, path: JoinablePathLike) -> tuple[str, str]: stripped_path = self.strip_protocol(path) if self.local_file: return os.path.split(stripped_path) head = self.parent(stripped_path) or self.root_marker if head == self.sep: tail = stripped_path[1:] elif head: tail = stripped_path[len(head) + 1 :] elif self.netloc_is_anchor: # and not head head = stripped_path tail = "" else: tail = stripped_path if ( not tail and not self.has_meaningful_trailing_slash and self.strip_protocol(head) != stripped_path ): return self.split(head) return head, tail def splitdrive(self, path: JoinablePathLike) -> tuple[str, str]: path = self.strip_protocol(path) if self.netloc_is_anchor: u = urlsplit(path) if u.scheme: # cases like: "http://example.com/foo/bar" drive = u._replace(path="", query="", fragment="").geturl() rest = u._replace(scheme="", netloc="").geturl() if ( u.path.startswith("//") and SplitResult("", "", "//", "", "").geturl() == "////" ): # see: fsspec/universal_pathlib#233 rest = rest[2:] return drive, rest or self.root_marker or self.sep else: # cases like: "bucket/some/special/key drive, root, tail = path.partition(self.sep) return drive, root + tail elif self.local_file: return os.path.splitdrive(path) else: # all other cases don't have a drive return "", path def normcase(self, path: JoinablePathLike) -> str: if self.local_file: return os.path.normcase(self.stringify_path(path)) else: return self.stringify_path(path) def splitext(self, path: JoinablePathLike) -> tuple[str, str]: path = self.stringify_path(path) if self.local_file: return os.path.splitext(path) else: path, sep, name = path.rpartition(self.sep) if "." in name: stem, dot, ext = name.rpartition(".") suffix = dot + ext else: stem = name suffix = "" return path + sep + stem, suffix # === Python3.12 pathlib flavour ================================== def splitroot(self, path: JoinablePathLike) -> tuple[str, str, str]: drive, tail = self.splitdrive(path) if self.netloc_is_anchor: root_marker = self.root_marker or self.sep else: root_marker = self.root_marker return drive, root_marker, tail.removeprefix(self.sep) default_flavour = WrappedFileSystemFlavour(AnyProtocolFileSystemFlavour) class LazyFlavourDescriptor: """descriptor to lazily get the flavour for a given protocol""" def __init__(self) -> None: self._owner: type[UPath] | None = None def __set_name__(self, owner: type[UPath], name: str) -> None: # helper to provide a more informative repr self._owner = owner self._default_protocol: str | None try: self._default_protocol = self._owner.protocols[0] # type: ignore except (AttributeError, IndexError): self._default_protocol = None def __get__( self, obj: UPath | None, objtype: type[UPath] | None = None ) -> WrappedFileSystemFlavour: if obj is not None: return WrappedFileSystemFlavour.from_protocol( obj._chain.active_path_protocol ) elif self._default_protocol: # type: ignore return WrappedFileSystemFlavour.from_protocol(self._default_protocol) else: return default_flavour def __repr__(self): cls_name = f"{type(self).__name__}" if self._owner is None: return f"" else: return f"<{cls_name} of {self._owner.__name__}>" def upath_strip_protocol(pth: JoinablePathLike) -> str: if protocol := get_upath_protocol(pth): return WrappedFileSystemFlavour.from_protocol(protocol).strip_protocol(pth) return WrappedFileSystemFlavour.stringify_path(pth) def upath_get_kwargs_from_url(url: JoinablePathLike) -> dict[str, Any]: if protocol := get_upath_protocol(url): return WrappedFileSystemFlavour.from_protocol(protocol).get_kwargs_from_url(url) return {} def upath_urijoin(base: str, uri: str) -> str: """Join a base URI and a possibly relative URI to form an absolute interpretation of the latter.""" # see: # https://github.com/python/cpython/blob/ae6c01d9d2/Lib/urllib/parse.py#L539-L605 # modifications: # - removed allow_fragments parameter # - all schemes are considered to allow relative paths # - all schemes are considered to allow netloc (revisit this) # - no bytes support (removes encoding and decoding) if not base: return uri if not uri: return base bs = urlsplit(base, scheme="") us = urlsplit(uri, scheme=bs.scheme) if us.scheme != bs.scheme: # or us.scheme not in uses_relative: return uri # if us.scheme in uses_netloc: if us.netloc: return us.geturl() else: us = us._replace(netloc=bs.netloc) # end if if not us.path and not us.fragment: us = us._replace(path=bs.path, fragment=bs.fragment) if not us.query: us = us._replace(query=bs.query) return us.geturl() base_parts = bs.path.split("/") if base_parts[-1] != "": del base_parts[-1] if us.path[:1] == "/": segments = us.path.split("/") else: segments = base_parts + us.path.split("/") segments[1:-1] = filter(None, segments[1:-1]) resolved_path: list[str] = [] for seg in segments: if seg == "..": try: resolved_path.pop() except IndexError: pass elif seg == ".": continue else: resolved_path.append(seg) if segments[-1] in (".", ".."): resolved_path.append("") return us._replace(path="/".join(resolved_path) or "/").geturl() universal_pathlib-0.3.10/upath/_flavour_sources.py000066400000000000000000001003341514661127100223660ustar00rootroot00000000000000""" upath._flavour_sources Warning ------- Do not modify this file manually! It is generated by `dev/generate_flavours.py` To be able to parse the different filesystem uri schemes, we need the string parsing functionality each of the filesystem implementations. In an attempt to support parsing uris without having to import the specific filesystems, we extract the necessary subset of the AbstractFileSystem classes and generate a new "flavour" class for each of the known filesystems. This will allow us to provide a `PurePath` equivalent `PureUPath` for each protocol in the future without a direct dependency on the underlying filesystem package. """ # # skipping protocols: # - blockcache # - cached # - dir # - filecache # protocol import errors: # - gdrive (Please install gdrive_fs for access to Google Drive) # - generic (GenericFileSystem: '_strip_protocol' not a classmethod) # - pyscript (Install requests (cpython) or run in pyscript) # - tos (Install tosfs to access ByteDance volcano engine Tinder Object Storage) # - tosfs (Install tosfs to access ByteDance volcano engine Tinder Object Storage) # from __future__ import annotations import logging import os import re from pathlib import PurePath from pathlib import PureWindowsPath from typing import Any from typing import Literal from typing import cast from urllib.parse import parse_qs from urllib.parse import urlsplit from fsspec.implementations.local import make_path_posix from fsspec.utils import infer_storage_options from fsspec.utils import stringify_path __all__ = [ "AbstractFileSystemFlavour", "FileSystemFlavourBase", "flavour_registry", ] logger = logging.getLogger(__name__) flavour_registry: dict[str, type[FileSystemFlavourBase]] = {} class FileSystemFlavourBase: """base class for the fsspec flavours""" protocol: str | tuple[str, ...] root_marker: Literal["/", ""] sep: Literal["/"] @classmethod def _strip_protocol(cls, path): raise NotImplementedError @staticmethod def _get_kwargs_from_urls(path): raise NotImplementedError @classmethod def _parent(cls, path): raise NotImplementedError def __init_subclass__(cls: Any, **kwargs): if isinstance(cls.protocol, str): protocols = (cls.protocol,) else: protocols = tuple(cls.protocol) for protocol in protocols: if protocol in flavour_registry: raise ValueError(f"protocol {protocol!r} already registered") flavour_registry[protocol] = cls class AbstractFileSystemFlavour(FileSystemFlavourBase): __orig_class__ = 'fsspec.spec.AbstractFileSystem' __orig_version__ = '2025.10.0' protocol: str | tuple[str, ...] = 'abstract' root_marker: Literal['', '/'] = '' sep: Literal['/'] = '/' @classmethod def _strip_protocol(cls, path): """Turn path from fully-qualified to file-system-specific May require FS-specific handling, e.g., for relative paths or links. """ if isinstance(path, list): return [cls._strip_protocol(p) for p in path] path = stringify_path(path) protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol for protocol in protos: if path.startswith(protocol + "://"): path = path[len(protocol) + 3 :] elif path.startswith(protocol + "::"): path = path[len(protocol) + 2 :] path = path.rstrip("/") # use of root_marker to make minimum required path, e.g., "/" return path or cls.root_marker @staticmethod def _get_kwargs_from_urls(path): """If kwargs can be encoded in the paths, extract them here This should happen before instantiation of the class; incoming paths then should be amended to strip the options in methods. Examples may look like an sftp path "sftp://user@host:/my/path", where the user and host should become kwargs and later get stripped. """ # by default, nothing happens return {} @classmethod def _parent(cls, path): path = cls._strip_protocol(path) if "/" in path: parent = path.rsplit("/", 1)[0].lstrip(cls.root_marker) return cls.root_marker + parent else: return cls.root_marker class AsyncFileSystemWrapperFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.asyn_wrapper.AsyncFileSystemWrapper' __orig_version__ = '2025.10.0' protocol = ('asyncwrapper', 'async_wrapper') root_marker = '' sep = '/' class AsyncLocalFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'morefs.asyn_local.AsyncLocalFileSystem' __orig_version__ = '0.2.2' protocol = () root_marker = '/' sep = '/' local_file = True @classmethod def _strip_protocol(cls, path): path = stringify_path(path) if path.startswith("file://"): path = path[7:] elif path.startswith("file:"): path = path[5:] elif path.startswith("local://"): path = path[8:] elif path.startswith("local:"): path = path[6:] path = make_path_posix(path) if os.sep != "/": # This code-path is a stripped down version of # > drive, path = ntpath.splitdrive(path) if path[1:2] == ":": # Absolute drive-letter path, e.g. X:\Windows # Relative path with drive, e.g. X:Windows drive, path = path[:2], path[2:] elif path[:2] == "//": # UNC drives, e.g. \\server\share or \\?\UNC\server\share # Device drives, e.g. \\.\device or \\?\device if (index1 := path.find("/", 2)) == -1 or ( index2 := path.find("/", index1 + 1) ) == -1: drive, path = path, "" else: drive, path = path[:index2], path[index2:] else: # Relative path, e.g. Windows drive = "" path = path.rstrip("/") or cls.root_marker return drive + path else: return path.rstrip("/") or cls.root_marker @classmethod def _parent(cls, path): path = cls._strip_protocol(path) if os.sep == "/": # posix native return path.rsplit("/", 1)[0] or "/" else: # NT path_ = path.rsplit("/", 1)[0] if len(path_) <= 3: if path_[1:2] == ":": # nt root (something like c:/) return path_[0] + ":/" # More cases may be required here return path_ class AzureBlobFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'adlfs.spec.AzureBlobFileSystem' __orig_version__ = '2025.8.0' protocol = ('abfs', 'az', 'abfss') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path: str): """ Remove the protocol from the input path Parameters ---------- path: str Path to remove the protocol from Returns ------- str Returns a path without the protocol """ if isinstance(path, list): # type: ignore[unreachable] return [cls._strip_protocol(p) for p in path] # type: ignore[unreachable] STORE_SUFFIX = ".dfs.core.windows.net" logger.debug(f"_strip_protocol for {path}") if not path.startswith(("abfs://", "az://", "abfss://")): path = path.lstrip("/") path = "abfs://" + path ops = infer_storage_options(path) if "username" in ops: if ops.get("username", None): ops["path"] = ops["username"] + ops["path"] # we need to make sure that the path retains # the format {host}/{path} # here host is the container_name elif ops.get("host", None): if ( ops["host"].count(STORE_SUFFIX) == 0 ): # no store-suffix, so this is container-name ops["path"] = ops["host"] + ops["path"] url_query = ops.get("url_query") if url_query is not None: ops["path"] = f"{ops['path']}?{url_query}" logger.debug(f"_strip_protocol({path}) = {ops}") stripped_path = ops["path"].lstrip("/") return stripped_path @staticmethod def _get_kwargs_from_urls(urlpath): """Get the account_name from the urlpath and pass to storage_options""" ops = infer_storage_options(urlpath) out = {} host = ops.get("host", None) if host: match = re.match( r"(?P.+)\.(dfs|blob)\.core\.windows\.net", host ) if match: account_name = match.groupdict()["account_name"] out["account_name"] = account_name url_query = ops.get("url_query") if url_query is not None: from urllib.parse import parse_qs parsed = parse_qs(url_query) if "versionid" in parsed: out["version_aware"] = True return out class AzureDatalakeFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'adlfs.gen1.AzureDatalakeFileSystem' __orig_version__ = '2025.8.0' protocol = ('adl',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): ops = infer_storage_options(path) return ops["path"] @staticmethod def _get_kwargs_from_urls(paths): """Get the store_name from the urlpath and pass to storage_options""" ops = infer_storage_options(paths) out = {} if ops.get("host", None): out["store_name"] = ops["host"] return out class BoxFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'boxfs.boxfs.BoxFileSystem' __orig_version__ = '0.3.0' protocol = ('box',) root_marker = '/' sep = '/' @classmethod def _strip_protocol(cls, path) -> str: path = super()._strip_protocol(path) path = path.replace("\\", "/") # Make all paths start with root marker if not path.startswith(cls.root_marker): path = cls.root_marker + path return path class DaskWorkerFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.dask.DaskWorkerFileSystem' __orig_version__ = '2025.10.0' protocol = ('dask',) root_marker = '' sep = '/' @staticmethod def _get_kwargs_from_urls(path): so = infer_storage_options(path) if "host" in so and "port" in so: return {"client": f"{so['host']}:{so['port']}"} else: return {} class DataFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.data.DataFileSystem' __orig_version__ = '2025.10.0' protocol = ('data',) root_marker = '' sep = '' # type: ignore[assignment] altsep = ' ' # type: ignore[assignment] class DatabricksFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.dbfs.DatabricksFileSystem' __orig_version__ = '2025.10.0' protocol = ('dbfs',) root_marker = '' sep = '/' class DictFSFlavour(AbstractFileSystemFlavour): __orig_class__ = 'morefs.dict.DictFS' __orig_version__ = '0.2.2' protocol = ('dictfs',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path: str) -> str: if path.startswith("dictfs://"): path = path[len("dictfs://") :] if "::" in path or "://" in path: return path.rstrip("/") path = path.lstrip("/").rstrip("/") return "/" + path if path else cls.root_marker class DropboxDriveFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'dropboxdrivefs.core.DropboxDriveFileSystem' __orig_version__ = '1.4.1' protocol = ('dropbox',) root_marker = '' sep = '/' class FTPFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.ftp.FTPFileSystem' __orig_version__ = '2025.10.0' protocol = ('ftp',) root_marker = '/' sep = '/' @classmethod def _strip_protocol(cls, path): return "/" + infer_storage_options(path)["path"].lstrip("/").rstrip("/") @staticmethod def _get_kwargs_from_urls(urlpath): out = infer_storage_options(urlpath) out.pop("path", None) out.pop("protocol", None) return out class GCSFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'gcsfs.core.GCSFileSystem' __orig_version__ = '2025.10.0' protocol = ('gs', 'gcs') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): if isinstance(path, list): return [cls._strip_protocol(p) for p in path] path = stringify_path(path) protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol for protocol in protos: if path.startswith(protocol + "://"): path = path[len(protocol) + 3 :] elif path.startswith(protocol + "::"): path = path[len(protocol) + 2 :] # use of root_marker to make minimum required path, e.g., "/" return path or cls.root_marker @classmethod def _get_kwargs_from_urls(cls, path): _, _, generation = cls._split_path(path, version_aware=True) if generation is not None: return {"version_aware": True} return {} @classmethod def _split_path(cls, path, version_aware=False): """ Normalise GCS path string into bucket and key. Parameters ---------- path : string Input path, like `gcs://mybucket/path/to/file`. Path is of the form: '[gs|gcs://]bucket[/key][?querystring][#fragment]' GCS allows object generation (object version) to be specified in either the URL fragment or the `generation` query parameter. When provided, the fragment will take priority over the `generation` query paramenter. Returns ------- (bucket, key, generation) tuple """ path = cls._strip_protocol(path).lstrip("/") if "/" not in path: return path, "", None bucket, keypart = path.split("/", 1) key = keypart generation = None if version_aware: parts = urlsplit(keypart) try: if parts.fragment: generation = parts.fragment elif parts.query: parsed = parse_qs(parts.query) if "generation" in parsed: generation = parsed["generation"][0] # Sanity check whether this could be a valid generation ID. If # it is not, assume that # or ? characters are supposed to be # part of the object name. if generation is not None: int(generation) key = parts.path except ValueError: generation = None return ( bucket, key, generation, ) class GistFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.gist.GistFileSystem' __orig_version__ = '2025.10.0' protocol = ('gist',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): """ Remove 'gist://' from the path, if present. """ # The default infer_storage_options can handle gist://username:token@id/file # or gist://id/file, but let's ensure we handle a normal usage too. # We'll just strip the protocol prefix if it exists. path = infer_storage_options(path).get("path", path) return path.lstrip("/") @staticmethod def _get_kwargs_from_urls(path): """ Parse 'gist://' style URLs into GistFileSystem constructor kwargs. For example: gist://:TOKEN@/file.txt gist://username:TOKEN@/file.txt """ so = infer_storage_options(path) out = {} if "username" in so and so["username"]: out["username"] = so["username"] if "password" in so and so["password"]: out["token"] = so["password"] if "host" in so and so["host"]: # We interpret 'host' as the gist ID out["gist_id"] = so["host"] # Extract SHA and filename from path if "path" in so and so["path"]: path_parts = so["path"].rsplit("/", 2)[-2:] if len(path_parts) == 2: if path_parts[0]: # SHA present out["sha"] = path_parts[0] if path_parts[1]: # filename also present out["filenames"] = [path_parts[1]] return out class GitFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.git.GitFileSystem' __orig_version__ = '2025.10.0' protocol = ('git',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): path = super()._strip_protocol(path).lstrip("/") if ":" in path: path = path.split(":", 1)[1] if "@" in path: path = path.split("@", 1)[1] return path.lstrip("/") @staticmethod def _get_kwargs_from_urls(path): path = path.removeprefix("git://") out = {} if ":" in path: out["path"], path = path.split(":", 1) if "@" in path: out["ref"], path = path.split("@", 1) return out class GithubFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.github.GithubFileSystem' __orig_version__ = '2025.10.0' protocol = ('github',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): opts = infer_storage_options(path) if "username" not in opts: return super()._strip_protocol(path) return opts["path"].lstrip("/") @staticmethod def _get_kwargs_from_urls(path): opts = infer_storage_options(path) if "username" not in opts: return {} out = {"org": opts["username"], "repo": opts["password"]} if opts["host"]: out["sha"] = opts["host"] return out class HTTPFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.http.HTTPFileSystem' __orig_version__ = '2025.10.0' protocol = ('http', 'https') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): """For HTTP, we always want to keep the full URL""" return path @classmethod def _parent(cls, path): # override, since _strip_protocol is different for URLs par = super()._parent(path) if len(par) > 7: # "http://..." return par return "" class HadoopFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.arrow.HadoopFileSystem' __orig_version__ = '2025.10.0' protocol = ('hdfs', 'arrow_hdfs') root_marker = '/' sep = '/' @classmethod def _strip_protocol(cls, path): ops = infer_storage_options(path) path = ops["path"] if path.startswith("//"): # special case for "hdfs://path" (without the triple slash) path = path[1:] return path @staticmethod def _get_kwargs_from_urls(path): ops = infer_storage_options(path) out = {} if ops.get("host", None): out["host"] = ops["host"] if ops.get("username", None): out["user"] = ops["username"] if ops.get("port", None): out["port"] = ops["port"] if ops.get("url_query", None): queries = parse_qs(ops["url_query"]) if queries.get("replication", None): out["replication"] = int(queries["replication"][0]) return out class HfFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'huggingface_hub.hf_file_system.HfFileSystem' __orig_version__ = '1.4.1' protocol = ('hf',) root_marker = '' sep = '/' class JupyterFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.jupyter.JupyterFileSystem' __orig_version__ = '2025.10.0' protocol = ('jupyter', 'jlab') root_marker = '' sep = '/' class LakeFSFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'lakefs_spec.spec.LakeFSFileSystem' __orig_version__ = '0.12.0' protocol = ('lakefs',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): """Copied verbatim from the base class, save for the slash rstrip.""" if isinstance(path, list): return [cls._strip_protocol(p) for p in path] spath = super()._strip_protocol(path) if stringify_path(path).endswith("/"): return spath + "/" return spath class LibArchiveFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.libarchive.LibArchiveFileSystem' __orig_version__ = '2025.10.0' protocol = ('libarchive',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): # file paths are always relative to the archive root return super()._strip_protocol(path).lstrip("/") class LocalFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.local.LocalFileSystem' __orig_version__ = '2025.10.0' protocol = ('file', 'local') root_marker = '/' sep = '/' local_file = True @classmethod def _strip_protocol(cls, path): path = stringify_path(path) if path.startswith("file://"): path = path[7:] elif path.startswith("file:"): path = path[5:] elif path.startswith("local://"): path = path[8:] elif path.startswith("local:"): path = path[6:] path = make_path_posix(path) if os.sep != "/": # This code-path is a stripped down version of # > drive, path = ntpath.splitdrive(path) if path[1:2] == ":": # Absolute drive-letter path, e.g. X:\Windows # Relative path with drive, e.g. X:Windows drive, path = path[:2], path[2:] elif path[:2] == "//": # UNC drives, e.g. \\server\share or \\?\UNC\server\share # Device drives, e.g. \\.\device or \\?\device if (index1 := path.find("/", 2)) == -1 or ( index2 := path.find("/", index1 + 1) ) == -1: drive, path = path, "" else: drive, path = path[:index2], path[index2:] else: # Relative path, e.g. Windows drive = "" path = path.rstrip("/") or cls.root_marker return drive + path else: return path.rstrip("/") or cls.root_marker @classmethod def _parent(cls, path): path = cls._strip_protocol(path) if os.sep == "/": # posix native return path.rsplit("/", 1)[0] or "/" else: # NT path_ = path.rsplit("/", 1)[0] if len(path_) <= 3: if path_[1:2] == ":": # nt root (something like c:/) return path_[0] + ":/" # More cases may be required here return path_ class MemFSFlavour(AbstractFileSystemFlavour): __orig_class__ = 'morefs.memory.MemFS' __orig_version__ = '0.2.2' protocol = ('memfs',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): if path.startswith("memfs://"): path = path[len("memfs://") :] return MemoryFileSystemFlavour._strip_protocol(path) # pylint: disable=protected-access class MemoryFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.memory.MemoryFileSystem' __orig_version__ = '2025.10.0' protocol = ('memory',) root_marker = '/' sep = '/' @classmethod def _strip_protocol(cls, path): if isinstance(path, PurePath): if isinstance(path, PureWindowsPath): return LocalFileSystemFlavour._strip_protocol(path) else: path = stringify_path(path) path = path.removeprefix("memory://") if "::" in path or "://" in path: return path.rstrip("/") path = path.lstrip("/").rstrip("/") return "/" + path if path else "" class OCIFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'ocifs.core.OCIFileSystem' __orig_version__ = '1.3.4' protocol = ('oci', 'ocilake') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): if isinstance(path, list): return [cls._strip_protocol(p) for p in path] path = stringify_path(path) stripped_path = super()._strip_protocol(path) if stripped_path == cls.root_marker and "@" in path: return "@" + path.rstrip("/").split("@", 1)[1] return stripped_path @classmethod def _parent(cls, path): path = cls._strip_protocol(path.rstrip("/")) if "/" in path: return cls.root_marker + path.rsplit("/", 1)[0] elif "@" in path: return cls.root_marker + "@" + path.split("@", 1)[1] else: raise ValueError(f"the following path does not specify a namespace: {path}") class OSSFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'ossfs.core.OSSFileSystem' __orig_version__ = '2025.5.0' protocol = ('oss',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): """Turn path from fully-qualified to file-system-specifi Parameters ---------- path : Union[str, List[str]] Input path, like `http://oss-cn-hangzhou.aliyuncs.com/mybucket/myobject` `oss://mybucket/myobject` Examples -------- >>> _strip_protocol( "http://oss-cn-hangzhou.aliyuncs.com/mybucket/myobject" ) ('/mybucket/myobject') >>> _strip_protocol( "oss://mybucket/myobject" ) ('/mybucket/myobject') """ if isinstance(path, list): return [cls._strip_protocol(p) for p in path] path_string = stringify_path(path) if path_string.startswith("oss://"): path_string = path_string[5:] parser_re = r"https?://(?Poss.+aliyuncs\.com)(?P/.+)" matcher = re.compile(parser_re).match(path_string) if matcher: path_string = matcher["path"] return path_string or cls.root_marker class OverlayFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'morefs.overlay.OverlayFileSystem' __orig_version__ = '0.2.2' protocol = ('overlayfs',) root_marker = '' sep = '/' class ReferenceFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.reference.ReferenceFileSystem' __orig_version__ = '2025.10.0' protocol = ('reference',) root_marker = '' sep = '/' class S3FileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 's3fs.core.S3FileSystem' __orig_version__ = '2025.10.0' protocol = ('s3', 's3a') root_marker = '' sep = '/' @staticmethod def _get_kwargs_from_urls(urlpath): """ When we have a urlpath that contains a ?versionId= Assume that we want to use version_aware mode for the filesystem. """ from urllib.parse import urlsplit url_query = urlsplit(urlpath).query out = {} if url_query is not None: from urllib.parse import parse_qs parsed = parse_qs(url_query) if "versionId" in parsed: out["version_aware"] = True return out class SFTPFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.sftp.SFTPFileSystem' __orig_version__ = '2025.10.0' protocol = ('sftp', 'ssh') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): return infer_storage_options(path)["path"] @staticmethod def _get_kwargs_from_urls(urlpath): out = infer_storage_options(urlpath) out.pop("path", None) out.pop("protocol", None) return out class SMBFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.smb.SMBFileSystem' __orig_version__ = '2025.10.0' protocol = ('smb',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): return infer_storage_options(path)["path"] @staticmethod def _get_kwargs_from_urls(path): # smb://workgroup;user:password@host:port/share/folder/file.csv out = infer_storage_options(path) out.pop("path", None) out.pop("protocol", None) return out class SimpleCacheFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.cached.SimpleCacheFileSystem' __orig_version__ = '2025.10.0' protocol = ('simplecache',) root_marker = '' sep = '/' local_file = True class TarFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.tar.TarFileSystem' __orig_version__ = '2025.10.0' protocol = ('tar',) root_marker = '' sep = '/' class WandbFSFlavour(AbstractFileSystemFlavour): __orig_class__ = 'wandbfs._wandbfs.WandbFS' __orig_version__ = '0.0.2' protocol = ('wandb',) root_marker = '' sep = '/' class WebHDFSFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.webhdfs.WebHDFS' __orig_version__ = '2025.10.0' protocol = ('webhdfs', 'webHDFS') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): return infer_storage_options(path)["path"] @staticmethod def _get_kwargs_from_urls(urlpath): out = infer_storage_options(urlpath) out.pop("path", None) out.pop("protocol", None) if "username" in out: out["user"] = out.pop("username") return out class WebdavFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'webdav4.fsspec.WebdavFileSystem' __orig_version__ = '0.10.0' protocol = ('webdav', 'dav') root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path: str) -> str: """Strips protocol from the given path, overriding for type-casting.""" stripped = super()._strip_protocol(path) return cast(str, stripped) class XRootDFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec_xrootd.xrootd.XRootDFileSystem' __orig_version__ = '0.5.1' protocol = ('root',) root_marker = '/' sep = '/' @classmethod def _strip_protocol(cls, path: str | list[str]) -> Any: if isinstance(path, str): if path.startswith(cls.protocol): x = urlsplit(path); return (x.path + f'?{x.query}' if x.query else '').rstrip("/") or cls.root_marker # assume already stripped return path.rstrip("/") or cls.root_marker elif isinstance(path, list): return [cls._strip_protocol(item) for item in path] else: raise ValueError("Strip protocol not given string or list") @staticmethod def _get_kwargs_from_urls(u: str) -> dict[Any, Any]: url = urlsplit(u) # The hostid encapsulates user,pass,host,port in one string return {"hostid": url.netloc} class ZipFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'fsspec.implementations.zip.ZipFileSystem' __orig_version__ = '2025.10.0' protocol = ('zip',) root_marker = '' sep = '/' @classmethod def _strip_protocol(cls, path): # zip file paths are always relative to the archive root return super()._strip_protocol(path).lstrip("/") class _DVCFileSystemFlavour(AbstractFileSystemFlavour): __orig_class__ = 'dvc.fs.dvc._DVCFileSystem' __orig_version__ = '3.66.1' protocol = ('dvc',) root_marker = '/' sep = '/' universal_pathlib-0.3.10/upath/_info.py000066400000000000000000000013041514661127100200750ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from upath.types import PathInfo if TYPE_CHECKING: from upath import UPath __all__ = [ "UPathInfo", ] class UPathInfo(PathInfo): """Path info for UPath objects.""" def __init__(self, path: UPath) -> None: self._path = path.path self._fs = path.fs def exists(self, *, follow_symlinks=True) -> bool: return self._fs.exists(self._path) def is_dir(self, *, follow_symlinks=True) -> bool: return self._fs.isdir(self._path) def is_file(self, *, follow_symlinks=True) -> bool: return self._fs.isfile(self._path) def is_symlink(self) -> bool: return False universal_pathlib-0.3.10/upath/_protocol.py000066400000000000000000000102731514661127100210100ustar00rootroot00000000000000from __future__ import annotations import re from collections import ChainMap from pathlib import PurePath from typing import TYPE_CHECKING from typing import Any from fsspec.registry import known_implementations as _known_implementations from fsspec.registry import registry as _registry if TYPE_CHECKING: from upath.types import JoinablePathLike __all__ = [ "get_upath_protocol", "normalize_empty_netloc", "compatible_protocol", ] # Regular expression to match fsspec style protocols. # Matches single slash usage too for compatibility. _PROTOCOL_RE = re.compile( r"^(?P[A-Za-z][A-Za-z0-9+]+):(?:(?P//?)|:)(?P.*)" ) # Matches data URIs _DATA_URI_RE = re.compile(r"^data:[^,]*,") def _match_protocol(pth: str) -> str: if m := _PROTOCOL_RE.match(pth): return m.group("protocol") elif _DATA_URI_RE.match(pth): return "data" return "" _fsspec_registry_map = ChainMap(_registry, _known_implementations) def _fsspec_protocol_equals(p0: str, p1: str) -> bool: """check if two fsspec protocols are equivalent""" p0 = p0 or "file" p1 = p1 or "file" if p0 == p1: return True try: o0 = _fsspec_registry_map[p0] except KeyError: raise ValueError(f"Protocol not known: {p0!r}") try: o1 = _fsspec_registry_map[p1] except KeyError: raise ValueError(f"Protocol not known: {p1!r}") if o0 == o1: return True if isinstance(o0, dict): o0 = o0.get("class") elif isinstance(o0, type): if o0.__module__: o0 = o0.__module__ + "." + o0.__name__ else: o0 = o0.__name__ if isinstance(o1, dict): o1 = o1.get("class") elif isinstance(o1, type): if o1.__module__: o1 = o1.__module__ + "." + o1.__name__ else: o1 = o1.__name__ return o0 == o1 def get_upath_protocol( pth: JoinablePathLike, *, protocol: str | None = None, storage_options: dict[str, Any] | None = None, ) -> str: """return the filesystem spec protocol""" from upath.core import UPath if isinstance(pth, str): pth_protocol = _match_protocol(pth) elif isinstance(pth, UPath): pth_protocol = pth.protocol elif isinstance(pth, PurePath): pth_protocol = getattr(pth, "protocol", "") elif hasattr(pth, "__vfspath__"): pth_protocol = _match_protocol(pth.__vfspath__()) elif hasattr(pth, "__fspath__"): pth_protocol = _match_protocol(pth.__fspath__()) else: pth_protocol = _match_protocol(str(pth)) # if storage_options and not protocol and not pth_protocol: # protocol = "file" if protocol is None: return pth_protocol or "" elif ( protocol and pth_protocol and not _fsspec_protocol_equals(pth_protocol, protocol) ): raise ValueError( f"requested protocol {protocol!r} incompatible with {pth_protocol!r}" ) elif protocol == "" and pth_protocol: # explicitly requested empty protocol, but path has non-empty protocol raise ValueError( f"explicitly requested empty protocol {protocol!r}" f" incompatible with {pth_protocol!r}" ) return protocol or pth_protocol or "" def normalize_empty_netloc(pth: str) -> str: if m := _PROTOCOL_RE.match(pth): if m.group("slashes") == "/": protocol = m.group("protocol") path = m.group("path") pth = f"{protocol}:///{path}" return pth def compatible_protocol( protocol: str, *args: JoinablePathLike, ) -> bool: """check if UPath protocols are compatible""" from upath.core import UPath for arg in args: if isinstance(arg, UPath) and not arg.is_absolute(): # relative UPath are always compatible continue other_protocol = get_upath_protocol(arg) # consider protocols equivalent if they match up to the first "+" other_protocol = other_protocol.partition("+")[0] # protocols: only identical (or empty "") protocols can combine if other_protocol and not _fsspec_protocol_equals(other_protocol, protocol): return False return True universal_pathlib-0.3.10/upath/_stat.py000066400000000000000000000327341514661127100201300ustar00rootroot00000000000000from __future__ import annotations import os import warnings from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from datetime import datetime from stat import S_IFDIR from stat import S_IFLNK from stat import S_IFREG from typing import Any __all__ = [ "UPathStatResult", ] def _convert_value_to_timestamp(value: Any) -> int | float: """Try to convert a datetime-like value to a timestamp.""" if isinstance(value, (int, float)): return value elif isinstance(value, str): if len(value) == 14: return datetime.strptime(value, r"%Y%m%d%H%M%S").timestamp() if value.endswith("Z"): value = value[:-1] + "+00:00" return datetime.fromisoformat(value).timestamp() elif isinstance(value, datetime): return value.timestamp() else: warnings.warn( f"Cannot convert {value!r} of type {type(value)!r} to a timestamp." " Please report this at: https://github.com/fsspec/universal_path/issues", RuntimeWarning, stacklevel=2, ) raise TypeError(f"Cannot convert {value!r} to a timestamp.") def _get_stat_result_extra_fields() -> tuple[str, ...]: """retrieve the extra fields of the os.stat_result class.""" # Note: # The lines below let us provide a dictionary with the additional # named fields of the stat_result class as keys and the internal # index of the field as value. sr = os.stat_result(range(os.stat_result.n_fields)) rd = sr.__reduce__() assert isinstance(rd, tuple), "unexpected return os.stat_result.__reduce__" _, (_, extra) = rd extra_fields = sorted(extra, key=extra.__getitem__) return tuple(extra_fields) class UPathStatResult: """A stat_result compatible class wrapping fsspec info dicts. **Note**: It is unlikely that you will ever have to instantiate this class directly. If you want to convert and info dict, use: `UPathStatResult.from_info(info)` This object may be accessed either as a tuple of (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) or via the attributes st_mode, st_ino, st_dev, st_nlink, st_uid, and so on. There's an additional method `as_info()` for accessing the info dict. This is useful to access additional information provided by the file system implementation, that's not covered by the stat_result tuple. """ __slots__ = ("_seq", "_info") # Note: # can't derive from os.stat_result at all, and can't derive from # tuple and have slots. So we duck type the os.stat_result class # Add the fields and "extra fields" of the os.stat_result class _fields = ( "st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size", "st_atime", "st_mtime", "st_ctime", ) _fields_extra = _get_stat_result_extra_fields() # Provide the n_ attributes of the os.stat_result class for compatibility n_sequence_fields = len(_fields) n_fields = len(_fields) + len(_fields_extra) n_unnamed_fields = len(set(_fields_extra).intersection(_fields)) if ( n_fields != os.stat_result.n_fields or n_sequence_fields != os.stat_result.n_sequence_fields or n_unnamed_fields != os.stat_result.n_unnamed_fields ): warnings.warn( "UPathStatResult: The assumed number of fields in the" " stat_result class is not correct. Got: " f" {_fields!r}, {_fields_extra!r}, {os.stat_result.n_fields}" " This might cause problems? Please report this issue at:" " https://github.com/fsspec/universal_path/issues", RuntimeWarning, stacklevel=2, ) def __init__( self, stat_result_seq: Sequence[int], info_dict: Mapping[str, Any] | None = None, ) -> None: """init compatible with os.stat_result Use `UPathStatResult.from_info(info)` to instantiate from a fsspec info. """ seq = tuple(stat_result_seq) if n := len(seq) < self.n_sequence_fields: raise TypeError( f"{self.__name__} takes at least {self.n_fields}-sequence" " ({n}-sequence given)" ) elif n > self.n_fields: raise TypeError( f"{self.__name__} takes at most {self.n_fields}-sequence" " ({n}-sequence given)" ) elif self.n_sequence_fields <= n < self.n_sequence_fields: warnings.warn( "UPathStatResult: The seq provided more than" f" {self.n_sequence_fields} items. Ignoring the extra items...", UserWarning, stacklevel=2, ) self._seq = seq[: self.n_sequence_fields] self._info = info_dict or {} def __repr__(self): cls_name = type(self).__name__ seq_attrs = ", ".join(map("{0[0]}={0[1]}".format, zip(self._fields, self))) return f"{cls_name}({seq_attrs}, info={self._info!r})" def __eq__(self, other): if not isinstance(other, UPathStatResult): return NotImplemented else: return self._info == other._info # --- access to the fsspec info dict ------------------------------ @classmethod def from_info(cls, info: Mapping[str, Any]) -> UPathStatResult: """Create a UPathStatResult from a fsspec info dict.""" # fill all the fallback default values with 0 defaults = [0] * cls.n_sequence_fields return cls(defaults, info) def as_info(self) -> Mapping[str, Any]: """Return the fsspec info dict.""" return self._info # --- guaranteed fields ------------------------------------------- @property def st_mode(self) -> int: """protection bits""" mode = self._info.get("mode") if isinstance(mode, int): return mode elif isinstance(mode, str): try: return int(mode, 8) except ValueError: pass type_ = self._info.get("type") if type_ == "file": return S_IFREG # see: stat.S_ISREG elif type_ == "directory": return S_IFDIR # see: stat.S_ISDIR if self._info.get("isLink"): return S_IFLNK # see: stat.S_ISLNK return self._seq[0] @property def st_ino(self) -> int: """inode""" ino = self._info.get("ino") if isinstance(ino, int): return ino return self._seq[1] @property def st_dev(self) -> int: """device""" dev = self._info.get("dev") if isinstance(dev, int): return dev return self._seq[2] @property def st_nlink(self) -> int: """number of hard links""" nlink = self._info.get("nlink") if isinstance(nlink, int): return nlink return self._seq[3] @property def st_uid(self) -> int: """user ID of owner""" for key in ["uid", "owner", "uname", "unix.owner"]: try: return int(self._info[key]) except (ValueError, TypeError, KeyError): pass return self._seq[4] @property def st_gid(self) -> int: """group ID of owner""" for key in ["gid", "group", "gname", "unix.group"]: try: return int(self._info[key]) except (ValueError, TypeError, KeyError): pass return self._seq[5] @property def st_size(self) -> int: """total size, in bytes""" try: return int(self._info["size"]) except (ValueError, TypeError, KeyError): return self._seq[6] @property def st_atime(self) -> int | float: """time of last access""" for key in ["atime", "time", "last_accessed", "accessTime"]: try: raw_value = self._info[key] except KeyError: continue try: return _convert_value_to_timestamp(raw_value) except (TypeError, ValueError): pass return self._seq[7] @property def st_mtime(self) -> int | float: """time of last modification""" for key in [ "mtime", "LastModified", "last_modified", "timeModified", "modificationTime", "modified_at", "modify", ]: try: raw_value = self._info[key] except KeyError: continue try: return _convert_value_to_timestamp(raw_value) except (TypeError, ValueError): pass return self._seq[8] @property def st_ctime(self) -> int | float: """time of last change""" try: raw_value = self._info["ctime"] except KeyError: pass else: try: return _convert_value_to_timestamp(raw_value) except (TypeError, ValueError): pass return self._seq[9] @property def st_birthtime(self) -> int | float: """time of creation""" for key in [ "birthtime", "created", "creation_time", "timeCreated", "created_at", ]: try: raw_value = self._info[key] except KeyError: continue try: return _convert_value_to_timestamp(raw_value) except (TypeError, ValueError): pass raise AttributeError("birthtime") @property def st_atime_ns(self) -> int: """time of last access in nanoseconds""" try: return int(self._info["atime_ns"]) except KeyError: pass atime = self.st_atime if isinstance(atime, float): return int(atime * 1e9) return atime * 1_000_000_000 @property def st_mtime_ns(self) -> int: """time of last modification in nanoseconds""" try: return int(self._info["mtime_ns"]) except KeyError: pass mtime = self.st_mtime if isinstance(mtime, float): return int(mtime * 1e9) return mtime * 1_000_000_000 @property def st_ctime_ns(self) -> int: """time of last change in nanoseconds""" try: return int(self._info["ctime_ns"]) except KeyError: pass ctime = self.st_ctime if isinstance(ctime, float): return int(ctime * 1e9) return ctime * 1_000_000_000 # --- extra fields ------------------------------------------------ def __getattr__(self, item): if item in self._fields_extra: return 0 # fallback default value raise AttributeError(item) # --- os.stat_result tuple interface ------------------------------ def __len__(self) -> int: return len(self._fields) def __iter__(self) -> Iterator[int]: """the sequence interface iterates over the guaranteed fields. All values are integers. """ for field in self._fields: yield int(getattr(self, field)) def index(self, value: int, start: int = 0, stop: int | None = None, /) -> int: """the sequence interface index method.""" if stop is None: stop = len(self._seq) return self._seq.index(value, start, stop) def count(self, value: int) -> int: """the sequence interface count method.""" return self._seq.count(value) # --- compatibility with the fsspec info dict interface ------------ def __getitem__(self, item: int | str) -> Any: if isinstance(item, str): warnings.warn( "Access the fsspec info via `.as_info()[key]`", DeprecationWarning, stacklevel=2, ) return self._info[item] # we need to go via the attributes and cast to int attr = self._fields[item] return int(getattr(self, attr)) def keys(self): """compatibility with the fsspec info dict interface.""" warnings.warn( "Access the fsspec info via `.as_info().keys()`", DeprecationWarning, stacklevel=2, ) return self._info.keys() def values(self): """compatibility with the fsspec info dict interface.""" warnings.warn( "Access the fsspec info via `.as_info().values()`", DeprecationWarning, stacklevel=2, ) return self._info.values() def items(self): """compatibility with the fsspec info dict interface.""" warnings.warn( "Access the fsspec info via `.as_info().items()`", DeprecationWarning, stacklevel=2, ) return self._info.items() def get(self, key, default=None): """compatibility with the fsspec info dict interface.""" warnings.warn( "Access the fsspec info via `.as_info().get(key, default)`", DeprecationWarning, stacklevel=2, ) return self._info.get(key, default) def copy(self): """compatibility with the fsspec info dict interface.""" warnings.warn( "Access the fsspec info via `.as_info().copy()`", DeprecationWarning, stacklevel=2, ) return self._info.copy() universal_pathlib-0.3.10/upath/core.py000066400000000000000000002307621514661127100177470ustar00rootroot00000000000000"""upath.core module: UPath base class implementation""" from __future__ import annotations import sys import warnings from abc import ABCMeta from abc import abstractmethod from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from copy import copy from pathlib import PurePath from types import MappingProxyType from typing import IO from typing import TYPE_CHECKING from typing import Any from typing import BinaryIO from typing import Literal from typing import NoReturn from typing import TextIO from typing import TypeVar from typing import overload from urllib.parse import SplitResult from urllib.parse import urlsplit from fsspec.registry import get_filesystem_class from fsspec.spec import AbstractFileSystem from upath._chain import DEFAULT_CHAIN_PARSER from upath._chain import Chain from upath._chain import FSSpecChainParser from upath._flavour import LazyFlavourDescriptor from upath._flavour import WrappedFileSystemFlavour from upath._flavour import upath_get_kwargs_from_url from upath._flavour import upath_urijoin from upath._info import UPathInfo from upath._protocol import compatible_protocol from upath._protocol import get_upath_protocol from upath._stat import UPathStatResult from upath.registry import _get_implementation_protocols from upath.registry import available_implementations from upath.registry import get_upath_class from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike from upath.types import OnNameCollisionFunc from upath.types import PathInfo from upath.types import ReadablePath from upath.types import ReadablePathLike from upath.types import StatResultType from upath.types import SupportsPathLike from upath.types import UPathParser from upath.types import WritablePath from upath.types import WritablePathLike if sys.version_info >= (3, 13): from pathlib import UnsupportedOperation else: UnsupportedOperation = NotImplementedError """Raised when an unsupported operation is called on a path object.""" if TYPE_CHECKING: import upath.implementations as _uimpl if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self from pydantic import GetCoreSchemaHandler from pydantic_core.core_schema import CoreSchema _MT = TypeVar("_MT") _WT = TypeVar("_WT", bound="WritablePath") __all__ = [ "UPath", "UnsupportedOperation", ] _FSSPEC_HAS_WORKING_GLOB = None def _check_fsspec_has_working_glob(): global _FSSPEC_HAS_WORKING_GLOB from fsspec.implementations.memory import MemoryFileSystem m = type("_M", (MemoryFileSystem,), {"store": {}, "pseudo_dirs": [""]})() m.touch("a.txt") m.touch("f/b.txt") g = _FSSPEC_HAS_WORKING_GLOB = len(m.glob("**/*.txt")) == 2 return g def _make_instance(cls, args, kwargs): """helper for pickling UPath instances""" # Extract _relative_base if present relative_base = kwargs.pop("_relative_base", None) instance = cls(*args, **kwargs) if relative_base is not None: instance._relative_base = relative_base return instance def _buffering2blocksize(mode: str, buffering: int) -> int | None: if not isinstance(buffering, int): raise TypeError("buffering must be an integer") if buffering == 0: # buffering disabled if "b" not in mode: # text mode raise ValueError("can't have unbuffered text I/O") return buffering elif buffering == -1: return None else: return buffering def _raise_unsupported(cls_name: str, method: str) -> NoReturn: raise UnsupportedOperation(f"{cls_name}.{method}() is unsupported") class _IncompatibleProtocolError(TypeError, ValueError): """switch to TypeError for incompatible protocols in a backward compatible way. !!! Do not use this exception directly !!! Catch TypeError instead, if you need to handle incompatible protocol errors. We'll do the switch in a future major release. """ # evil: make this look like a built-in TypeError __module__ = "builtins" __qualname__ = "TypeError" def __repr__(self) -> str: return f"TypeError({', '.join(map(repr, self.args))})" class _UPathMeta(ABCMeta): """metaclass for UPath to customize instance creation There are two main reasons for this metaclass: - support copying UPath instances via UPath(existing_upath) - force calling __init__ on instance creation for instances of a non-subclass """ if sys.version_info < (3, 11): # pathlib 3.9 and 3.10 supported `Path[str]` but # did not return a GenericAlias but the class itself? def __getitem__(cls, key): return cls def __call__(cls: type[_MT], *args: Any, **kwargs: Any) -> _MT: # create a copy if UPath class try: (arg0,) = args except ValueError: pass else: if isinstance(arg0, UPath) and not kwargs: return copy(arg0) # type: ignore[return-value] # We do this call manually, because cls could be a registered # subclass of UPath that is not directly inheriting from UPath. inst = cls.__new__(cls, *args, **kwargs) inst.__init__(*args, **kwargs) # type: ignore[misc] return inst class _UPathMixin(metaclass=_UPathMeta): """Mixin class for UPath to allow sharing some common functionality between UPath and PosixUPath/WindowsUPath. """ __slots__ = () @property @abstractmethod def parser(self) -> UPathParser: """The parser (flavour) for this UPath instance.""" raise NotImplementedError @property def _protocol(self) -> str: return self._chain.nest().protocol @_protocol.setter def _protocol(self, value: str) -> None: self._chain = self._chain.replace(protocol=value) @property def _storage_options(self) -> dict[str, Any]: return self._chain.nest().storage_options @_storage_options.setter def _storage_options(self, value: dict[str, Any]) -> None: self._chain = self._chain.replace(storage_options=value) @property @abstractmethod def _chain(self) -> Chain: raise NotImplementedError @_chain.setter @abstractmethod def _chain(self, value: Chain) -> None: raise NotImplementedError @property @abstractmethod def _chain_parser(self) -> FSSpecChainParser: raise NotImplementedError @_chain_parser.setter @abstractmethod def _chain_parser(self, value: FSSpecChainParser) -> None: raise NotImplementedError @property @abstractmethod def _fs_cached(self) -> AbstractFileSystem: raise NotImplementedError @_fs_cached.setter def _fs_cached(self, value: AbstractFileSystem): raise NotImplementedError @property @abstractmethod def _raw_urlpaths(self) -> Sequence[JoinablePathLike]: raise NotImplementedError @_raw_urlpaths.setter def _raw_urlpaths(self, value: Sequence[JoinablePathLike]) -> None: raise NotImplementedError @property @abstractmethod def _relative_base(self) -> str | None: raise NotImplementedError @_relative_base.setter def _relative_base(self, value: str | None) -> None: raise NotImplementedError # === upath.UPath PUBLIC ADDITIONAL API =========================== @property def protocol(self) -> str: """The fsspec protocol for the path. Note ---- Protocols are linked to upath and fsspec filesystems via the `upath.registry` and `fsspec.registry` modules. They basically represent the URI scheme used for the specific filesystem. Examples -------- >>> from upath import UPath >>> p0 = UPath("s3://my-bucket/path/to/file.txt") >>> p0.protocol 's3' >>> p1 = UPath("/foo/bar/baz.txt", protocol="memory") >>> p1.protocol 'memory' """ return self._protocol @property def storage_options(self) -> Mapping[str, Any]: """The read-only fsspec storage options for the path. Note ---- Storage options are specific to each fsspec filesystem and can include parameters such as authentication credentials, connection settings, and other options that affect how the filesystem interacts with the underlying storage. Examples -------- >>> from upath import UPath >>> p = UPath("s3://my-bucket/path/to/file.txt", anon=True) >>> p.storage_options['anon'] True """ return MappingProxyType(self._storage_options) @property def fs(self) -> AbstractFileSystem: """The cached fsspec filesystem instance for the path. This is the underlying fsspec filesystem instance. It's instantiated on first filesystem access and cached. Can be used to access fsspec-specific functionality not exposed by the UPath API. Examples -------- >>> from upath import UPath >>> p = UPath("s3://my-bucket/path/to/file.txt") >>> p.fs >>> p.fs.get_tags(p.path) {'VersionId': 'null', 'ContentLength': 12345, ...} """ try: return self._fs_cached except AttributeError: fs = self._fs_cached = self._fs_factory( str(self), self.protocol, self.storage_options ) return fs @property def path(self) -> str: """The path used by fsspec filesystem. FSSpec filesystems usually handle paths stripped of protocol. This property returns the path suitable for use with the underlying fsspec filesystem. It guarantees that a filesystem's strip_protocol method is applied correctly. Examples -------- >>> from upath import UPath >>> p = UPath("memory:///foo/bar.txt") >>> str(p) 'memory:///foo/bar.txt' >>> p.path '/foo/bar.txt' >>> p.fs.exists(p.path) True """ if self._relative_base is not None: try: # For relative paths, we need to resolve to absolute path current_dir = self.cwd() # type: ignore[attr-defined] except NotImplementedError: raise UnsupportedOperation( f"fsspec paths can not be relative and" f" {type(self).__name__}.cwd() is unsupported" ) from None # Join the current directory with the relative path if (self_path := str(self)) == ".": path = str(current_dir) else: path = current_dir.parser.join(str(current_dir), self_path) return self.parser.strip_protocol(path) return self._chain.active_path def joinuri(self, uri: JoinablePathLike) -> UPath: """Join with urljoin behavior for UPath instances. Examples -------- >>> from upath import UPath >>> p = UPath("https://example.com/dir/subdir/") >>> p.joinuri("file.txt") HTTPSPath('https://example.com/dir/subdir/file.txt') >>> p.joinuri("/anotherdir/otherfile.txt") HTTPSPath('https://example.com/anotherdir/otherfile.txt') >>> p.joinuri("memory:///foo/bar.txt" MemoryPath('memory:///foo/bar.txt') """ # short circuit if the new uri uses a different protocol other_protocol = get_upath_protocol(uri) if other_protocol and other_protocol != self._protocol: return UPath(uri) return UPath( upath_urijoin(str(self), str(uri)), protocol=other_protocol or self._protocol, **self.storage_options, ) # === upath.UPath CUSTOMIZABLE API ================================ @classmethod def _transform_init_args( cls, args: tuple[JoinablePathLike, ...], protocol: str, storage_options: dict[str, Any], ) -> tuple[tuple[JoinablePathLike, ...], str, dict[str, Any]]: """allow customization of init args in subclasses""" return args, protocol, storage_options @classmethod def _parse_storage_options( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any], ) -> dict[str, Any]: """Parse storage_options from the urlpath""" pth_storage_options = upath_get_kwargs_from_url(urlpath) return {**pth_storage_options, **storage_options} @classmethod def _fs_factory( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any], ) -> AbstractFileSystem: """Instantiate the filesystem_spec filesystem class""" fs_cls = get_filesystem_class(protocol) return fs_cls(**storage_options) # === upath.UPath constructor ===================================== _protocol_dispatch: bool | None = None def __new__( # noqa C901 cls, *args: JoinablePathLike, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> UPath: # narrow type if not issubclass(cls, UPath): raise TypeError("UPath.__new__ can't instantiate non-UPath classes") # deprecate 'scheme' if "scheme" in storage_options: warnings.warn( "use 'protocol' kwarg instead of 'scheme'", DeprecationWarning, stacklevel=2, ) protocol = storage_options.pop("scheme") # determine the protocol try: pth_protocol = get_upath_protocol( args[0] if args else "", protocol=protocol, storage_options=storage_options, ) except ValueError as e: if "incompatible with" in str(e): raise _IncompatibleProtocolError(str(e)) from e raise # subclasses should default to their own protocol if protocol is None and cls is not UPath: impl_protocols = _get_implementation_protocols(cls) if not pth_protocol and impl_protocols: pth_protocol = impl_protocols[0] elif pth_protocol and pth_protocol not in impl_protocols: msg_protocol = pth_protocol if not pth_protocol: msg_protocol = "'' (empty string)" msg = ( f"{cls.__name__!s}(...) detected protocol {msg_protocol!s}" f" which is incompatible with {cls.__name__}." ) if not pth_protocol or pth_protocol not in available_implementations(): msg += ( " Did you forget to register the subclass for this protocol" " with upath.registry.register_implementation()?" ) raise _IncompatibleProtocolError(msg) # determine which UPath subclass to dispatch to upath_cls: type[UPath] | None if cls._protocol_dispatch or cls._protocol_dispatch is None: upath_cls = get_upath_class(protocol=pth_protocol) if upath_cls is None: raise ValueError(f"Unsupported filesystem: {pth_protocol!r}") else: # user subclasses can request to disable protocol dispatch # by setting MyUPathSubclass._protocol_dispatch to `False`. # This will effectively ignore the registered UPath # implementations and return an instance of MyUPathSubclass. # This be useful if a subclass wants to extend the UPath # api, and it is fine to rely on the default implementation # for all supported user protocols. # # THIS IS DEPRECATED! # Use upath.extensions.ProxyUPath to extend the UPath API warnings.warn( f"{cls.__name__}._protocol_dispatch = False is deprecated and" " will be removed in future universal_pathlib versions." " To extend the UPath API, subclass upath.extensions.ProxyUPath", DeprecationWarning, stacklevel=2, ) upath_cls = cls if issubclass(upath_cls, cls): pass elif not issubclass(upath_cls, UPath): raise RuntimeError("UPath.__new__ expected cls to be subclass of UPath") else: msg_protocol = pth_protocol if not pth_protocol: msg_protocol = "'' (empty string)" msg = ( f"{cls.__name__!s}(...) detected protocol {msg_protocol!s}" f" which is incompatible with {cls.__name__}." ) if ( # find a better way (not pth_protocol and cls.__name__ not in ["CloudPath", "LocalPath"]) or pth_protocol and pth_protocol not in available_implementations() ): msg += ( " Did you forget to register the subclass for this protocol" " with upath.registry.register_implementation()?" ) raise _IncompatibleProtocolError(msg) return object.__new__(upath_cls) def __init__( self, *args: JoinablePathLike, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> None: """Initialize a UPath instance When instantiating a `UPath`, the detected or provided protocol determines the `UPath` subclass that will be instantiated. The protocol is looked up via the `get_upath_protocol` function, which loads the registered `UPath` implementation from the registry. If no `UPath` implementation is found for the detected protocol, but a registered `fsspec` filesystem exists for the protocol, a default dynamically created `UPath` implementation will be used. Parameters ---------- *args : The path (or uri) segments to construct the UPath from. The first argument is used to detect the protocol if no protocol is provided. protocol : The protocol to use for the path. chain_parser : A chain parser instance for chained urlpaths. _(experimental)_ **storage_options : Additional storage options for the path. """ # todo: avoid duplicating this call from __new__ protocol = get_upath_protocol( args[0] if args else "", protocol=protocol, storage_options=storage_options, ) args, protocol, storage_options = type(self)._transform_init_args( args, protocol, storage_options ) # check that UPath subclasses in args are compatible # TODO: # Future versions of UPath could verify that storage_options # can be combined between UPath instances. Not sure if this # is really necessary though. A warning might be enough... if not compatible_protocol(protocol, *args): raise ValueError("can't combine incompatible UPath protocols") # subclasses should default to their own protocol if not protocol: impl_protocols = _get_implementation_protocols(type(self)) if impl_protocols: protocol = impl_protocols[0] if args: args0 = args[0] if isinstance(args0, UPath): storage_options = { **args0._chain.nest().storage_options, **storage_options, } str_args0 = args0.__vfspath__() else: if hasattr(args0, "__fspath__") and args0.__fspath__ is not None: str_args0 = args0.__fspath__() elif hasattr(args0, "__vfspath__") and args0.__vfspath__ is not None: str_args0 = args0.__vfspath__() elif isinstance(args0, str): str_args0 = args0 else: raise TypeError( "argument should be a UPath, str, " f"or support __vfspath__ or __fspath__, not {type(args0)!r}" ) storage_options = type(self)._parse_storage_options( str_args0, protocol, storage_options ) else: str_args0 = "." segments = chain_parser.unchain( str_args0, protocol=protocol, storage_options=storage_options, ) # FIXME: normalization needs to happen in unchain already... chain = Chain.from_list(Chain.from_list(segments).to_list()) if len(args) > 1: flavour = WrappedFileSystemFlavour.from_protocol(chain.active_path_protocol) joined = flavour.join(chain.active_path, *args[1:]) stripped = flavour.strip_protocol(joined) chain = chain.replace(path=stripped) self._chain = chain self._chain_parser = chain_parser self._raw_urlpaths = args self._relative_base = None # --- deprecated attributes --------------------------------------- @property def _url(self) -> SplitResult: # TODO: # _url should be deprecated, but for now there is no good way of # accessing query parameters from urlpaths... return urlsplit(self.__str__()) class UPath(_UPathMixin, WritablePath, ReadablePath): """Base class for pathlike paths backed by an fsspec filesystem. Note ---- The following attributes and methods are specific to UPath instances and are not available on pathlib.Path instances. Attributes ---------- protocol : The fsspec protocol for the path. storage_options : The fsspec storage options for the path. path : The path that a fsspec filesystem can use. fs : The cached fsspec filesystem instance for the path. Methods ------- joinuri(*parts) : Join URI parts to this path. Info ---- Below are pathlib attributes and methods available on UPath instances. Attributes ---------- drive : The drive component of the path. root : The root component of the path. anchor : The concatenation of the drive and root. parent : The logical parent of the path. parents : An immutable sequence providing access to the logical ancestors of the path. name : The final path component, excluding the drive and root, if any. suffix : The file extension of the final component, if any. suffixes : A list of the path's file extensions. stem : The final path component, without its suffix. info : Filesystem information about the path. parser : The path parser instance for parsing path segments. Methods ------- __truediv__(key) : Combine this path with the argument using the `/` operator. __rtruediv__(key) : Combine the argument with this path using the `/` operator. as_posix() : Return the string representation of the path with forward slashes. is_absolute() : Return True if the path is absolute. is_relative_to(other) : Return True if the path is relative to another path. is_reserved() : Return True if the path is reserved under Windows. joinpath(*pathsegments) : Combine this path with one or several arguments, and return a new path. full_match(pattern, *, case_sensitive=None) : Match this path against the provided glob-style pattern. match(pattern, *, case_sensitive=None) : Match this path against the provided glob-style pattern. relative_to(other, walk_up=False) : Return a version of this path relative to another path. with_name(name) : Return a new path with the name changed. with_stem(stem) : Return a new path with the stem changed. with_suffix(suffix) : Return a new path with the suffix changed. with_segments(*pathsegments) : Construct a new path object from any number of path-like objects. from_uri(uri) : Return a new path from the given URI. as_uri() : Return the path as a URI. home() : Return a new path pointing to the user's home directory. expanduser() : Return a new path with expanded `~` constructs. cwd() : Return a new path pointing to the current working directory. absolute() : Make the path absolute, without normalization or resolving symlinks. resolve(strict=False) : Make the path absolute, resolving any symlinks. readlink() : Return the path to which the symbolic link points. stat(*, follow_symlinks=True) : Return the result of the stat() system call on this path. lstat() : Like stat(), but if the path points to a symlink, return the symlink's information. exists(*, follow_symlinks=True) : Return True if the path exists. is_file(*, follow_symlinks=True) : Return True if the path is a regular file. is_dir(*, follow_symlinks=True) : Return True if the path is a directory. is_symlink() : Return True if the path is a symbolic link. is_junction() : Return True if the path is a junction. is_mount() : Return True if the path is a mount point. is_socket() : Return True if the path is a socket. is_fifo() : Return True if the path is a FIFO. is_block_device() : Return True if the path is a block device. is_char_device() : Return True if the path is a character device. samefile(other_path) : Return True if this path points to the same file as other_path. open(mode='r', buffering=-1, encoding=None, errors=None, newline=None) : Open the file pointed to by the path. read_text(encoding=None, errors=None, newline=None) : Open the file in text mode, read it, and close the file. read_bytes() : Open the file in bytes mode, read it, and close the file. write_text(data, encoding=None, errors=None, newline=None) : Open the file in text mode, write to it, and close the file. write_bytes(data) : Open the file in bytes mode, write to it, and close the file. iterdir() : Yield path objects of the directory contents. glob(pattern, *, case_sensitive=None) : Iterate over this subtree and yield all existing files matching the given pattern. rglob(pattern, *, case_sensitive=None) : Recursively yield all existing files matching the given pattern. walk(top_down=True, on_error=None, follow_symlinks=False) : Generate the file names in a directory tree by walking the tree. touch(mode=0o666, exist_ok=True) : Create this file with the given access mode, if it doesn't exist. mkdir(mode=0o777, parents=False, exist_ok=False) : Create a new directory at this given path. symlink_to(target, target_is_directory=False) : Make this path a symbolic link pointing to target. hardlink_to(target) : Make this path a hard link pointing to the same file as target. copy(target, *, follow_symlinks=True, preserve_metadata=False) : Copy the contents of this file to the target file. copy_into(target_dir, *, follow_symlinks=True, preserve_metadata=False) : Copy this file or directory into the target directory. rename(target) : Rename this path to the target path. replace(target) : Rename this path to the target path, overwriting if that path exists. move(target) : Move this file or directory tree to the target path. move_into(target_dir) : Move this file or directory into the target directory. unlink(missing_ok=False) : Remove this file or link. rmdir() : Remove this directory. owner(*, follow_symlinks=True) : Return the login name of the file owner. group(*, follow_symlinks=True) : Return the group name of the file gid. chmod(mode, *, follow_symlinks=True) : Change the permissions of the path. lchmod(mode) : Like chmod() but, if the path points to a symlink, modify the symlink's permissions. """ __slots__ = ( "_chain", "_chain_parser", "_fs_cached", "_raw_urlpaths", "_relative_base", ) if TYPE_CHECKING: # noqa: C901 _chain: Chain _chain_parser: FSSpecChainParser _fs_cached: AbstractFileSystem _raw_urlpaths: Sequence[JoinablePathLike] _relative_base: str | None @overload def __new__( cls, ) -> Self: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["simplecache"], **_: Any, ) -> _uimpl.cached.SimpleCachePath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["gcs", "gs"], **_: Any, ) -> _uimpl.cloud.GCSPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["s3", "s3a"], **_: Any, ) -> _uimpl.cloud.S3Path: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["az", "abfs", "abfss", "adl"], **_: Any, ) -> _uimpl.cloud.AzurePath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["hf"], **_: Any, ) -> _uimpl.cloud.HfPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["data"], **_: Any, ) -> _uimpl.data.DataPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["ftp"], **_: Any, ) -> _uimpl.ftp.FTPPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["github"], **_: Any, ) -> _uimpl.github.GitHubPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["hdfs"], **_: Any, ) -> _uimpl.hdfs.HDFSPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["http", "https"], **_: Any, ) -> _uimpl.http.HTTPPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["file", "local"], **_: Any, ) -> _uimpl.local.FilePath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["memory"], **_: Any, ) -> _uimpl.memory.MemoryPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["sftp", "ssh"], **_: Any, ) -> _uimpl.sftp.SFTPPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["smb"], **_: Any, ) -> _uimpl.smb.SMBPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["tar"], **_: Any, ) -> _uimpl.tar.TarPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["webdav"], **_: Any, ) -> _uimpl.webdav.WebdavPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal["zip"], **_: Any, ) -> _uimpl.zip.ZipPath: ... if sys.platform == "win32": @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal[""], **_: Any, ) -> _uimpl.local.WindowsUPath: ... else: @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: Literal[""], **_: Any, ) -> _uimpl.local.PosixUPath: ... @overload # noqa: E301 def __new__( cls, *args: JoinablePathLike, protocol: str | None = ..., **_: Any, ) -> Self: ... def __new__( cls, *args: JoinablePathLike, protocol: str | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Any, ) -> Self: ... # === JoinablePath attributes ===================================== parser: UPathParser = LazyFlavourDescriptor() # type: ignore[assignment] def with_segments(self, *pathsegments: JoinablePathLike) -> Self: """Construct a new path object from any number of path-like objects.""" # we change joinpath behavior if called from a relative path # this is not fully ideal, but currently the best way to move forward if is_relative := self._relative_base is not None: pathsegments = (self._relative_base, *pathsegments) new_instance = type(self)( *pathsegments, protocol=self._protocol, **self.storage_options, ) if hasattr(self, "_fs_cached"): new_instance._fs_cached = self._fs_cached if is_relative: new_instance._relative_base = self._relative_base return new_instance def __str__(self) -> str: if self._relative_base is not None: active_path = self._chain.active_path stripped_base = self.parser.strip_protocol( self._relative_base ).removesuffix(self.parser.sep) if not active_path.startswith(stripped_base): raise RuntimeError( f"{active_path!r} is not a subpath of {stripped_base!r}" ) return ( active_path.removeprefix(stripped_base).removeprefix(self.parser.sep) or "." ) else: return self._chain_parser.chain(self._chain.to_list())[0] def __vfspath__(self) -> str: if self._relative_base is not None: return self.__str__() else: return self.path def __repr__(self) -> str: cls_name = type(self).__name__ path = self.__vfspath__() if self._relative_base is not None: return f"" else: return f"{cls_name}({path!r}, protocol={self._protocol!r})" # === JoinablePath overrides ====================================== @property def parts(self) -> Sequence[str]: """Provides sequence-like access to the filesystem path components. Examples -------- >>> from upath import UPath >>> p = UPath("s3://my-bucket/path/to/file.txt") >>> p.parts ('my-bucket/', 'path', 'to', 'file.txt') >>> p2 = UPath("/foo/bar/baz.txt", protocol="memory") >>> p2.parts ('/', 'foo', 'bar', 'baz.txt') """ # For relative paths, return parts of the relative path only if self._relative_base is not None: rel_str = str(self) if rel_str == ".": return () return tuple(rel_str.split(self.parser.sep)) split = self.parser.split sep = self.parser.sep path = self._chain.active_path drive = self.parser.splitdrive(self._chain.active_path)[0] stripped_path = self.parser.strip_protocol(path) if stripped_path: _, _, tail = path.partition(stripped_path) path = stripped_path + tail parent, name = split(path) names = [] while path != parent: names.append(name) path = parent parent, name = split(path) if names and names[-1] == drive: names = names[:-1] if names and names[-1].startswith(sep): parts = [*names[:-1], names[-1].removeprefix(sep), drive + sep] else: parts = [*names, drive + sep] return tuple(reversed(parts)) def with_name(self, name: str) -> Self: """Return a new path with the file name changed.""" split = self.parser.split if self.parser.sep in name: # `split(name)[0]` raise ValueError(f"Invalid name {name!r}") _path = self.__vfspath__() _path = _path.removesuffix(split(_path)[1]) + name return self.with_segments(_path) @property def anchor(self) -> str: """The concatenation of the drive and root or an empty string.""" if self._relative_base is not None: return "" return self.drive + self.root @property def parent(self) -> Self: """The logical parent of the path. Examples -------- >>> from upath import UPath >>> p = UPath("s3://my-bucket/path/to/file.txt") >>> p.parent S3Path('s3://my-bucket/path/to') """ if self._relative_base is not None: if str(self) == ".": return self else: # this needs to be revisited... pth = type(self)( self._relative_base, str(self), protocol=self._protocol, **self.storage_options, ) parent = pth.parent parent._relative_base = self._relative_base return parent return super().parent @property def parents(self) -> Sequence[Self]: """A sequence providing access to the logical ancestors of the path. Examples -------- >>> from upath import UPath >>> p = UPath("memory:///foo/bar/baz.txt") >>> list(p.parents) [ MemoryPath('memory:///foo/bar'), MemoryPath('memory:///foo'), MemoryPath('memory:///'), ] """ if self._relative_base is not None: parents = [] parent = self while True: if str(parent) == ".": break parent = parent.parent parents.append(parent) return tuple(parents) return super().parents def joinpath(self, *pathsegments: JoinablePathLike) -> Self: """Combine this path with one or several arguments, and return a new path. For one argument, this is equivalent to using the `/` operator. Examples -------- >>> from upath import UPath >>> p = UPath("s3://my-bucket/path/to") >>> p.joinpath("file.txt") S3Path('s3://my-bucket/path/to/file.txt') """ return self.with_segments(self.__vfspath__(), *pathsegments) def __truediv__(self, key: JoinablePathLike) -> Self: try: return self.with_segments(self.__vfspath__(), key) except TypeError: return NotImplemented def __rtruediv__(self, key: JoinablePathLike) -> Self: try: return self.with_segments(key, self.__vfspath__()) except TypeError: return NotImplemented # === ReadablePath attributes ===================================== @property def info(self) -> PathInfo: """ A PathInfo object that exposes the file type and other file attributes of this path. Returns ------- : UPathInfo The UPathInfo object for this path. """ return UPathInfo(self) def iterdir(self) -> Iterator[Self]: """Yield path objects of the directory contents. Examples -------- >>> from upath import UPath >>> p = UPath("memory:///foo/") >>> p.joinpath("bar.txt").touch() >>> p.joinpath("baz.txt").touch() >>> for child in p.iterdir(): ... print(child) MemoryPath('memory:///foo/bar.txt') MemoryPath('memory:///foo/baz.txt') """ sep = self.parser.sep base = self if self.parts[-1:] == ("",): base = self.parent fs = base.fs base_path = base.path if not fs.isdir(base_path): raise NotADirectoryError(str(self)) for name in fs.listdir(base_path): # fsspec returns dictionaries if isinstance(name, dict): name = name.get("name") if name in {".", ".."}: # Yielding a path object for these makes little sense continue # only want the path name with iterdir _, _, name = name.removesuffix(sep).rpartition(self.parser.sep) yield base.with_segments(base_path, name) def __open_reader__(self) -> BinaryIO: return self.fs.open(self.path, mode="rb") if sys.version_info >= (3, 14): def __open_rb__(self, buffering: int = UNSET_DEFAULT) -> BinaryIO: return self.open("rb", buffering=buffering) def readlink(self) -> Self: _raise_unsupported(type(self).__name__, "readlink") @overload def copy(self, target: _WT, **kwargs: Any) -> _WT: ... @overload def copy(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... def copy(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath: """ Recursively copy this file or directory tree to the given destination. """ if isinstance(target, str): proto = get_upath_protocol(target) if proto != self.protocol: target = UPath(target) else: target = self.with_segments(target) elif not isinstance(target, UPath): target = UPath(target) if target.is_dir(): raise IsADirectoryError(str(target)) return super().copy(target, **kwargs) @overload def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @overload def copy_into(self, target_dir: SupportsPathLike | str, **kwargs: Any) -> Self: ... def copy_into( self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any ) -> _WT | UPath: """ Copy this file or directory tree into the given existing directory. """ if isinstance(target_dir, str): proto = get_upath_protocol(target_dir) if proto != self.protocol: target_dir = UPath(target_dir) else: target_dir = self.with_segments(target_dir) elif not isinstance(target_dir, UPath): target_dir = UPath(target_dir) if not target_dir.exists(): raise FileNotFoundError(str(target_dir)) if not target_dir.is_dir(): raise NotADirectoryError(str(target_dir)) return super().copy_into(target_dir, **kwargs) @overload def move(self, target: _WT, **kwargs: Any) -> _WT: ... @overload def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... def move(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath: """ Recursively move this file or directory tree to the given destination. """ target = self.copy(target, **kwargs) self.fs.rm(self.path, recursive=self.is_dir()) return target @overload def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @overload def move_into(self, target_dir: SupportsPathLike | str, **kwargs: Any) -> Self: ... def move_into( self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any ) -> _WT | UPath: """ Move this file or directory tree into the given existing directory. """ name = self.name if not name: raise ValueError(f"{self!r} has an empty name") elif hasattr(target_dir, "with_segments"): target = target_dir.with_segments(target_dir, name) # type: ignore elif isinstance(target_dir, PurePath): target = UPath(target_dir, name) else: target = self.with_segments(target_dir, name) td = target.parent if not td.exists(): raise FileNotFoundError(str(td)) elif not td.is_dir(): raise NotADirectoryError(str(td)) return self.move(target) def _copy_from( self, source: ReadablePath, follow_symlinks: bool = True, on_name_collision: OnNameCollisionFunc | None = None, **kwargs: Any, ) -> None: """ UPath custom:: Recursively copy the given path to this path. """ # fixme: it would be best if this would be upstreamed from pathlib_abc import vfsopen from pathlib_abc import vfspath from pathlib_abc._os import copyfileobj from pathlib_abc._os import ensure_different_files stack: list[tuple[ReadablePath, WritablePath]] = [(source, self)] while stack: src, dst = stack.pop() info = src.info if not follow_symlinks and info.is_symlink(): dst.symlink_to(vfspath(src.readlink()), src.info.is_dir()) elif on_name_collision and info.is_file() and info.is_dir(): dst_file, dst_dir = on_name_collision(src, dst) if dst_file is not None: ensure_different_files(src, dst_file) with vfsopen(src, "rb") as source_f: with vfsopen(dst_file, "wb") as target_f: copyfileobj(source_f, target_f) if dst_dir is not None: children = src.iterdir() dst_dir.mkdir() # feed through dict.fromkeys to remove duplicates for child in dict.fromkeys(children): stack.append((child, dst_dir.joinpath(child.name))) elif info.is_dir(): children = src.iterdir() dst.mkdir() # feed through dict.fromkeys to remove duplicates for child in dict.fromkeys(children): stack.append((child, dst.joinpath(child.name))) else: ensure_different_files(src, dst) with vfsopen(src, "rb") as source_f: with vfsopen(dst, "wb") as target_f: copyfileobj(source_f, target_f) # --- WritablePath attributes ------------------------------------- def symlink_to( self, target: ReadablePathLike, target_is_directory: bool = False, ) -> None: _raise_unsupported(type(self).__name__, "symlink_to") def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: """ Create a new directory at this given path. """ if parents and not exist_ok and self.exists(): raise FileExistsError(str(self)) try: self.fs.mkdir( self.path, create_parents=parents, mode=mode, ) except FileExistsError: if not exist_ok: raise FileExistsError(str(self)) if not self.is_dir(): raise FileExistsError(str(self)) def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: return self.fs.open(self.path, mode=f"{mode}b") # --- upath overrides --------------------------------------------- @overload def open( self, mode: Literal["r", "w", "a"] = ..., buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> TextIO: ... @overload def open( self, mode: Literal["rb", "wb", "ab"] = ..., buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> BinaryIO: ... @overload def open( self, mode: str = ..., buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., **fsspec_kwargs: Any, ) -> IO[Any]: ... def open( self, mode: str = "r", buffering: int = UNSET_DEFAULT, encoding: str | None = UNSET_DEFAULT, errors: str | None = UNSET_DEFAULT, newline: str | None = UNSET_DEFAULT, **fsspec_kwargs: Any, ) -> IO[Any]: """ Open the file pointed by this path and return a file object, as the built-in open() function does. Parameters ---------- mode: Opening mode. Default is 'r'. buffering: Default is the block size of the underlying fsspec filesystem. encoding: Encoding is only used in text mode. Default is None. errors: Error handling for encoding. Only used in text mode. Default is None. newline: Newline handling. Only used in text mode. Default is None. **fsspec_kwargs: Additional options for the fsspec filesystem. """ # match the signature of pathlib.Path.open() if buffering is not UNSET_DEFAULT: if "block_size" in fsspec_kwargs: raise TypeError("cannot specify both 'buffering' and 'block_size'") block_size = _buffering2blocksize(mode, buffering) if block_size is not None: fsspec_kwargs.setdefault("block_size", block_size) if encoding is not UNSET_DEFAULT: fsspec_kwargs["encoding"] = encoding if errors is not UNSET_DEFAULT: fsspec_kwargs["errors"] = errors if newline is not UNSET_DEFAULT: fsspec_kwargs["newline"] = newline return self.fs.open(self.path, mode=mode, **fsspec_kwargs) # === pathlib.Path ================================================ def stat( self, *, follow_symlinks: bool = True, ) -> StatResultType: """ Return the result of the stat() system call on this path, like os.stat() does. Info ---- For fsspec filesystems follow_symlinks is currently ignored. Returns ------- : UPathStatResult The upath stat result for this path, emulating `os.stat_result`. """ if not follow_symlinks: warnings.warn( f"{type(self).__name__}.stat(follow_symlinks=False):" " is currently ignored.", UserWarning, stacklevel=2, ) return UPathStatResult.from_info(self.fs.info(self.path)) def lstat(self) -> StatResultType: return self.stat(follow_symlinks=False) def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: _raise_unsupported(type(self).__name__, "chmod") def exists(self, *, follow_symlinks: bool = True) -> bool: """ Whether this path exists. Info ---- For fsspec filesystems follow_symlinks is currently ignored. """ if not follow_symlinks: warnings.warn( f"{type(self).__name__}.exists() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return self.fs.exists(self.path) def is_dir(self, *, follow_symlinks: bool = True) -> bool: """ Whether this path is a directory. """ if not follow_symlinks: warnings.warn( f"{type(self).__name__}.is_dir() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return self.fs.isdir(self.path) def is_file(self, *, follow_symlinks: bool = True) -> bool: """ Whether this path is a regular file. """ if not follow_symlinks: warnings.warn( f"{type(self).__name__}.is_file() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return self.fs.isfile(self.path) def is_mount(self) -> bool: """ Check if this path is a mount point Info ---- For fsspec filesystems this is always False. """ return False def is_symlink(self) -> bool: """ Whether this path is a symbolic link. """ try: info = self.fs.info(self.path) if "islink" in info: return bool(info["islink"]) except FileNotFoundError: return False return False def is_junction(self) -> bool: """ Whether this path is a junction. Info ---- For fsspec filesystems this is always False. """ return False def is_block_device(self) -> bool: """ Whether this path is a block device. Info ---- For fsspec filesystems this is always False. """ return False def is_char_device(self) -> bool: """ Whether this path is a character device. Info ---- For fsspec filesystems this is always False. """ return False def is_fifo(self) -> bool: """ Whether this path is a FIFO (named pipe). Info ---- For fsspec filesystems this is always False. """ return False def is_socket(self) -> bool: """ Whether this path is a socket. Info ---- For fsspec filesystems this is always False. """ return False def is_reserved(self) -> bool: """ Whether this path is reserved under Windows. Info ---- For fsspec filesystems this is always False. """ return False def expanduser(self) -> Self: """Return a new path with expanded `~` constructs. Info ---- For fsspec filesystems this is currently a no-op. """ return self def glob( self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern.""" if case_sensitive is not None: warnings.warn( "UPath.glob(): case_sensitive is currently ignored.", UserWarning, stacklevel=2, ) if recurse_symlinks: warnings.warn( "UPath.glob(): recurse_symlinks=True is currently ignored.", UserWarning, stacklevel=2, ) if self._relative_base is not None: self = self.absolute() path_pattern = self.joinpath(pattern).path sep = self.parser.sep base = self.path for name in self.fs.glob(path_pattern): name = name.removeprefix(base).removeprefix(sep) yield self.joinpath(name) def rglob( self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: """Recursively yield all existing files (of any kind, including directories) matching the given relative pattern, anywhere in this subtree. """ if case_sensitive is not None: warnings.warn( "UPath.glob(): case_sensitive is currently ignored.", UserWarning, stacklevel=2, ) if recurse_symlinks: warnings.warn( "UPath.glob(): recurse_symlinks=True is currently ignored.", UserWarning, stacklevel=2, ) if _FSSPEC_HAS_WORKING_GLOB is None: _check_fsspec_has_working_glob() if _FSSPEC_HAS_WORKING_GLOB: r_path_pattern = self.joinpath("**", pattern).path sep = self.parser.sep base = self.path for name in self.fs.glob(r_path_pattern): name = name.removeprefix(base).removeprefix(sep) yield self.joinpath(name) else: path_pattern = self.joinpath(pattern).path r_path_pattern = self.joinpath("**", pattern).path sep = self.parser.sep base = self.path seen = set() for p in (path_pattern, r_path_pattern): for name in self.fs.glob(p): name = name.removeprefix(base).removeprefix(sep) if name in seen: continue else: seen.add(name) yield self.joinpath(name) def owner(self, *, follow_symlinks: bool = True) -> str: _raise_unsupported(type(self).__name__, "owner") def group(self, *, follow_symlinks: bool = True) -> str: _raise_unsupported(type(self).__name__, "group") def absolute(self) -> Self: """Return an absolute version of this path No normalization or symlink resolution is performed. Use resolve() to resolve symlinks and remove '..' segments. """ if self._relative_base is not None: return self.cwd().joinpath(self.__vfspath__()) return self def is_absolute(self) -> bool: """True if the path is absolute (has both a root and, if applicable, a drive).""" if self._relative_base is not None: return False else: return self.parser.isabs(self.__vfspath__()) def __eq__(self, other: object) -> bool: """UPaths are considered equal if their protocol, path and storage_options are equal.""" if not isinstance(other, UPath): return NotImplemented # For relative paths, compare the string representation instead of path if ( self._relative_base is not None or getattr(other, "_relative_base", None) is not None ): # If both are relative paths, compare just the relative strings if ( self._relative_base is not None and getattr(other, "_relative_base", None) is not None ): return str(self) == str(other) else: # One is relative, one is not - they can't be equal return False return ( self.__vfspath__() == other.__vfspath__() and self.protocol == other.protocol and self.storage_options == other.storage_options ) def __hash__(self) -> int: """The returned hash is based on the protocol and path only. Note: in the future, if hash collisions become an issue, we can add `fsspec.utils.tokenize(storage_options)` """ return hash((self.protocol, self.__vfspath__())) def __lt__(self, other: object) -> bool: if not isinstance(other, UPath) or self.parser is not other.parser: return NotImplemented return self.__vfspath__() < other.__vfspath__() def __le__(self, other: object) -> bool: if not isinstance(other, UPath) or self.parser is not other.parser: return NotImplemented return self.__vfspath__() <= other.__vfspath__() def __gt__(self, other: object) -> bool: if not isinstance(other, UPath) or self.parser is not other.parser: return NotImplemented return self.__vfspath__() > other.__vfspath__() def __ge__(self, other: object) -> bool: if not isinstance(other, UPath) or self.parser is not other.parser: return NotImplemented return self.__vfspath__() >= other.__vfspath__() def resolve(self, strict: bool = False) -> Self: """ Make the path absolute, resolving all symlinks on the way and also normalizing it. """ if self._relative_base is not None: self = self.absolute() _parts = self.parts # Do not attempt to normalize path if no parts are dots if ".." not in _parts and "." not in _parts: return self resolved: list[str] = [] resolvable_parts = _parts[1:] for part in resolvable_parts: if part == "..": if resolved: resolved.pop() elif part != ".": resolved.append(part) return self.with_segments(*_parts[:1], *resolved) def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: """Create this file with the given access mode, if it doesn't exist.""" exists = self.fs.exists(self.path) if exists and not exist_ok: raise FileExistsError(str(self)) if not exists: try: self.fs.touch(self.path, truncate=True) except NotImplementedError: _raise_unsupported(type(self).__name__, "touch") else: try: self.fs.touch(self.path, truncate=False) except (NotImplementedError, ValueError): pass # unsupported by filesystem def lchmod(self, mode: int) -> None: _raise_unsupported(type(self).__name__, "lchmod") def unlink(self, missing_ok: bool = False) -> None: """ Remove this file or link. If the path is a directory, use rmdir() instead. """ if not self.exists(): if not missing_ok: raise FileNotFoundError(str(self)) return self.fs.rm(self.path, recursive=False) def rmdir(self, recursive: bool = True) -> None: # fixme: non-standard """ Remove this directory. Warning ------- This method is non-standard compared to pathlib.Path.rmdir(), as it supports a `recursive` parameter to remove non-empty directories and defaults to recursive deletion. This behavior is likely to change in future releases once `.delete()` is introduced. """ if not self.is_dir(): raise NotADirectoryError(str(self)) if not recursive and next(self.iterdir()): # type: ignore[arg-type] raise OSError(f"Not recursive and directory not empty: {self}") self.fs.rm(self.path, recursive=recursive) def rename( self, target: WritablePathLike, *, # note: non-standard compared to pathlib recursive: bool = UNSET_DEFAULT, maxdepth: int | None = UNSET_DEFAULT, **kwargs: Any, ) -> Self: """ Rename this file or directory to the given target. The target path may be absolute or relative. Relative paths are interpreted relative to the current working directory, *not* the directory of the Path object. Returns the new Path instance pointing to the target path. Info ---- For filesystems that don't have a root character, i.e. for which relative paths can be ambiguous, you can explicitly indicate a relative path via prefixing with `./` Warning ------- This method is non-standard compared to pathlib.Path.rename(), as it supports `recursive` and `maxdepth` parameters for directory moves. This will be revisited in future releases. It's better to use `.move()` or `.move_into()` to avoid running into future compatibility issues. """ # check protocol compatibility target_protocol = get_upath_protocol(target) if target_protocol and target_protocol != self.protocol: raise ValueError( f"expected protocol {self.protocol!r}, got: {target_protocol!r}" ) # ensure target is an absolute UPath if not isinstance(target, type(self)): if isinstance(target, (UPath, PurePath)): target_str = target.as_posix() else: target_str = str(target) if target_protocol: # target protocol provided indicates absolute path target = self.with_segments(target_str) elif self.anchor and target_str.startswith(self.anchor): # self.anchor can be used to indicate absolute path target = self.with_segments(target_str) elif not self.anchor and target_str.startswith("./"): # indicate relative via "./" target = ( self.cwd() .joinpath(target_str.removeprefix("./")) .relative_to(self.cwd()) ) else: # all other cases target = self.cwd().joinpath(target_str).relative_to(self.cwd()) # return early if renaming to same path if target == self: return self # ensure source and target are absolute source_abs = self.absolute() target_abs = target.absolute() # avoid calling .resolve for if not needed if ".." in target_abs.parts or "." in target_abs.parts: target_abs = target_abs.resolve() if kwargs: warnings.warn( "Passing additional keyword arguments to " f"{type(self).__name__}.rename() is deprecated and will be" " removed in future univeral-pathlib versions.", DeprecationWarning, stacklevel=2, ) if recursive is not UNSET_DEFAULT: warnings.warn( f"{type(self).__name__}.rename()'s `recursive` keyword argument is" " deprecated and will be removed in future universal-pathlib versions." f" Please use {type(self).__name__}.move() or .move_into() instead.", DeprecationWarning, stacklevel=2, ) kwargs["recursive"] = recursive if maxdepth is not UNSET_DEFAULT: warnings.warn( f"{type(self).__name__}.rename()'s `maxdepth` keyword argument is" " deprecated and will be removed in future universal-pathlib versions.", DeprecationWarning, stacklevel=2, ) kwargs["maxdepth"] = maxdepth self.fs.mv( source_abs.path, target_abs.path, **kwargs, ) return target def replace(self, target: WritablePathLike) -> Self: """ Rename this path to the target path, overwriting if that path exists. The target path may be absolute or relative. Relative paths are interpreted relative to the current working directory, *not* the directory of the Path object. Returns the new Path instance pointing to the target path. Warning ------- This method is currently not implemented. """ _raise_unsupported(type(self).__name__, "replace") @property def drive(self) -> str: """The drive prefix (letter or UNC path), if any. Info ---- On non-Windows systems, the drive is always an empty string. On cloud storage systems, the drive is the bucket name or equivalent. """ if self._relative_base is not None: return "" return self.parser.splitroot(str(self))[0] @property def root(self) -> str: """The root of the path, if any.""" if self._relative_base is not None: return "" return self.parser.splitroot(str(self))[1] def __reduce__(self): if self._relative_base is None: args = (self.__vfspath__(),) kwargs = { "protocol": self._protocol, **self.storage_options, } else: args = (self._relative_base, self.__vfspath__()) # Include _relative_base in the state if it's set kwargs = { "protocol": self._protocol, **self.storage_options, "_relative_base": self._relative_base, } return _make_instance, (type(self), args, kwargs) @classmethod def from_uri(cls, uri: str, **storage_options: Any) -> Self: return cls(uri, **storage_options) def as_uri(self) -> str: """Return the string representation of the path as a URI.""" if self._relative_base is not None: raise ValueError( f"relative path can't be expressed as a {self.protocol} URI" ) return str(self) def as_posix(self) -> str: """Return the string representation of the path with POSIX-style separators.""" return str(self) def samefile(self, other_path) -> bool: st = self.stat() if isinstance(other_path, UPath): other_st = other_path.stat() else: other_st = self.with_segments(other_path).stat() return st == other_st @classmethod def cwd(cls) -> Self: """ Return a new UPath object representing the current working directory. Info ---- None of the fsspec filesystems support a global current working directory, so this method only works for the base UPath class, returning the local current working directory. """ if cls is UPath: # default behavior for UPath.cwd() is to return local cwd return get_upath_class("").cwd() # type: ignore[union-attr,return-value] else: _raise_unsupported(cls.__name__, "cwd") @classmethod def home(cls) -> Self: """ Return a new UPath object representing the user's home directory. Info ---- None of the fsspec filesystems support user home directories, so this method only works for the base UPath class, returning the local user's home directory. """ if cls is UPath: return get_upath_class("").home() # type: ignore[union-attr,return-value] else: _raise_unsupported(cls.__name__, "home") def relative_to( # type: ignore[override] self, other: Self | str, /, *_deprecated: Any, walk_up: bool = False, ) -> Self: """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError. The *walk_up* parameter controls whether `..` may be used to resolve the path. """ if walk_up: raise NotImplementedError("walk_up=True is not implemented yet") if isinstance(other, UPath): # revisit: ... if self.__class__ is not other.__class__: raise ValueError( "incompatible protocols:" f" {self.protocol!r} != {other.protocol!r}" ) if self.storage_options != other.storage_options: raise ValueError( "incompatible storage_options:" f" {self.storage_options!r} != {other.storage_options!r}" ) elif isinstance(other, str): other = self.with_segments(other) else: raise TypeError(f"expected UPath or str, got {type(other).__name__}") if other not in self.parents and self != other: raise ValueError(f"{self!s} is not in the subpath of {other!s}") else: rel = copy(self) rel._relative_base = other.path return rel def is_relative_to( self, other: Self | str, /, *_deprecated: Any, ) -> bool: # type: ignore[override] """Return True if the path is relative to another path identified.""" if isinstance(other, UPath) and self.storage_options != other.storage_options: return False elif isinstance(other, str): other = self.with_segments(other) return self == other or other in self.parents def hardlink_to(self, target: ReadablePathLike) -> None: _raise_unsupported(type(self).__name__, "hardlink_to") def full_match( self, pattern: str | SupportsPathLike, *, case_sensitive: bool | None = None, ) -> bool: """Match this path against the provided glob-style pattern. Return True if matching is successful, False otherwise. """ if case_sensitive is not None: warnings.warn( f"{type(self).__name__}.full_match(): case_sensitive" " is currently ignored.", UserWarning, stacklevel=2, ) return super().full_match(str(pattern)) def match( self, path_pattern: str | SupportsPathLike, *, case_sensitive: bool | None = None, ) -> bool: """Match this path against the provided non-recursive glob-style pattern. Return True if matching is successful, False otherwise. """ path_pattern = str(path_pattern) if not path_pattern: raise ValueError("pattern cannot be empty") if case_sensitive is not None: warnings.warn( f"{type(self).__name__}.match(): case_sensitive is currently ignored.", UserWarning, stacklevel=2, ) return self.full_match(path_pattern.replace("**", "*")) @classmethod def __get_pydantic_core_schema__( cls, _source_type: Any, _handler: GetCoreSchemaHandler ) -> CoreSchema: from pydantic_core import core_schema str_schema = core_schema.chain_schema( [ core_schema.str_schema(), core_schema.no_info_plain_validator_function( lambda path: { "path": path, "protocol": None, "storage_options": {}, }, ), ] ) object_schema = core_schema.typed_dict_schema( { "path": core_schema.typed_dict_field( core_schema.str_schema(), required=True ), "protocol": core_schema.typed_dict_field( core_schema.with_default_schema( core_schema.nullable_schema( core_schema.str_schema(), ), default=None, ), required=False, ), "storage_options": core_schema.typed_dict_field( core_schema.with_default_schema( core_schema.dict_schema( core_schema.str_schema(), core_schema.any_schema(), ), default_factory=dict, ), required=False, ), }, extra_behavior="forbid", ) deserialization_schema = core_schema.chain_schema( [ core_schema.union_schema([str_schema, object_schema]), core_schema.no_info_plain_validator_function( lambda dct: cls( dct.pop("path"), protocol=dct.pop("protocol"), **dct["storage_options"], ) ), ] ) serialization_schema = core_schema.plain_serializer_function_ser_schema( lambda u: { "path": u.path, "protocol": u.protocol, "storage_options": dict(u.storage_options), } ) return core_schema.json_or_python_schema( json_schema=deserialization_schema, python_schema=core_schema.union_schema( [core_schema.is_instance_schema(UPath), deserialization_schema] ), serialization=serialization_schema, ) universal_pathlib-0.3.10/upath/extensions.py000066400000000000000000000430321514661127100212060ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Iterator from collections.abc import Mapping from collections.abc import Sequence from typing import IO from typing import TYPE_CHECKING from typing import Any from typing import BinaryIO from typing import Callable from typing import Literal from typing import TextIO from typing import TypeVar from typing import overload from urllib.parse import SplitResult from fsspec import AbstractFileSystem from pathlib_abc import vfspath from upath._chain import Chain from upath._chain import ChainSegment from upath.core import UnsupportedOperation from upath.core import UPath from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike from upath.types import OnNameCollisionFunc from upath.types import PathInfo from upath.types import ReadablePath from upath.types import ReadablePathLike from upath.types import StatResultType from upath.types import SupportsPathLike from upath.types import UPathParser from upath.types import WritablePathLike if TYPE_CHECKING: if sys.version_info > (3, 11): from typing import Self else: from typing_extensions import Self from pydantic import GetCoreSchemaHandler from pydantic_core.core_schema import CoreSchema __all__ = [ "ProxyUPath", ] T = TypeVar("T") class classmethod_or_method(classmethod): """A decorator that can be used as a classmethod or an instance method. When called on the class, it behaves like a classmethod. When called on an instance, it behaves like an instance method. """ def __get__( self, instance: T | None, owner: type[T] | None = None, /, ) -> Callable[..., T]: if instance is None: return self.__func__.__get__(owner) else: return self.__func__.__get__(instance) class ProxyUPath: """ProxyUPath base class ProxyUPath should be used when you want to extend the UPath class interface with additional methods, but still want to support all supported upath implementations. """ __slots__ = ("__wrapped__",) # TODO: think about if and how to handle these # _transform_init_args # _parse_storage_options # _fs_factory # _protocol_dispatch # === non-public methods / attributes ============================= @classmethod def _from_upath(cls, upath: UPath, /) -> Self: if isinstance(upath, cls): return upath # type: ignore[unreachable] else: obj = object.__new__(cls) obj.__wrapped__ = upath return obj @property def _chain(self): try: return self.__wrapped__._chain except AttributeError: return Chain( ChainSegment( path=self.__wrapped__.path, protocol=self.__wrapped__.protocol, storage_options=dict(self.__wrapped__.storage_options), ), ) # === wrapped interface =========================================== def __init__( self, *args: JoinablePathLike, protocol: str | None = None, **storage_options: Any, ) -> None: self.__wrapped__ = UPath(*args, protocol=protocol, **storage_options) @property def parser(self) -> UPathParser: return self.__wrapped__.parser def with_segments(self, *pathsegments: JoinablePathLike) -> Self: return self._from_upath(self.__wrapped__.with_segments(*pathsegments)) def __str__(self) -> str: return self.__wrapped__.__str__() def __vfspath__(self) -> str: return self.__wrapped__.__vfspath__() def __repr__(self) -> str: return ( f"{type(self).__name__}" f"({self.__wrapped__.path!r}, protocol={self.protocol!r})" ) @property def parts(self) -> Sequence[str]: return self.__wrapped__.parts def with_name(self, name: str) -> Self: return self._from_upath(self.__wrapped__.with_name(name)) @property def info(self) -> PathInfo: return self.__wrapped__.info def iterdir(self) -> Iterator[Self]: for pth in self.__wrapped__.iterdir(): yield self._from_upath(pth) def __open_reader__(self) -> BinaryIO: return self.__wrapped__.__open_reader__() def readlink(self) -> Self: return self._from_upath(self.__wrapped__.readlink()) def symlink_to( self, target: ReadablePathLike, target_is_directory: bool = False, ) -> None: if not isinstance(target, str): target = vfspath(target) self.__wrapped__.symlink_to(target, target_is_directory=target_is_directory) def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: self.__wrapped__.mkdir(mode=mode, parents=parents, exist_ok=exist_ok) def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: return self.__wrapped__.__open_writer__(mode) @overload def open( self, mode: Literal["r", "w", "a"] = "r", buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> TextIO: ... @overload def open( self, mode: Literal["rb", "wb", "ab"], buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> BinaryIO: ... @overload def open( self, mode: str, *args: Any, **fsspec_kwargs: Any, ) -> IO[Any]: ... def open( self, mode: str = "r", buffering: int = UNSET_DEFAULT, encoding: str | None = UNSET_DEFAULT, errors: str | None = UNSET_DEFAULT, newline: str | None = UNSET_DEFAULT, **fsspec_kwargs: Any, ) -> IO[Any]: return self.__wrapped__.open( mode, buffering, encoding, errors, newline, **fsspec_kwargs, ) def stat( self, *, follow_symlinks=True, ) -> StatResultType: return self.__wrapped__.stat(follow_symlinks=follow_symlinks) def lstat(self) -> StatResultType: return self.__wrapped__.stat(follow_symlinks=False) def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: self.__wrapped__.chmod(mode=mode, follow_symlinks=follow_symlinks) def exists(self, *, follow_symlinks=True) -> bool: return self.__wrapped__.exists(follow_symlinks=follow_symlinks) def is_dir(self) -> bool: return self.__wrapped__.is_dir() def is_file(self) -> bool: return self.__wrapped__.is_file() def is_mount(self) -> bool: return self.__wrapped__.is_mount() def is_symlink(self) -> bool: return self.__wrapped__.is_symlink() def is_junction(self) -> bool: return self.__wrapped__.is_junction() def is_block_device(self) -> bool: return self.__wrapped__.is_block_device() def is_char_device(self) -> bool: return self.__wrapped__.is_char_device() def is_fifo(self) -> bool: return self.__wrapped__.is_fifo() def is_socket(self) -> bool: return self.__wrapped__.is_socket() def is_reserved(self) -> bool: return self.__wrapped__.is_reserved() def expanduser(self) -> Self: return self._from_upath(self.__wrapped__.expanduser()) def glob( self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: for p in self.__wrapped__.glob( pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks ): yield self._from_upath(p) def rglob( self, pattern: str, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: for p in self.__wrapped__.rglob( pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks ): yield self._from_upath(p) def owner(self) -> str: return self.__wrapped__.owner() def group(self) -> str: return self.__wrapped__.group() def absolute(self) -> Self: return self._from_upath(self.__wrapped__.absolute()) def is_absolute(self) -> bool: return self.__wrapped__.is_absolute() def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__eq__(other.__wrapped__) def __hash__(self) -> int: return self.__wrapped__.__hash__() def __ne__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__ne__(other.__wrapped__) def __lt__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__lt__(other.__wrapped__) def __le__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__le__(other.__wrapped__) def __gt__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__gt__(other.__wrapped__) def __ge__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.__wrapped__.__ge__(other.__wrapped__) def resolve(self, strict: bool = False) -> Self: return self._from_upath(self.__wrapped__.resolve(strict=strict)) def touch(self, mode=0o666, exist_ok=True) -> None: self.__wrapped__.touch(mode=mode, exist_ok=exist_ok) def lchmod(self, mode: int) -> None: self.__wrapped__.lchmod(mode=mode) def unlink(self, missing_ok: bool = False) -> None: self.__wrapped__.unlink(missing_ok=missing_ok) def rmdir(self, recursive: bool = UNSET_DEFAULT) -> None: # fixme: non-standard kwargs: dict[str, Any] = {} if recursive is not UNSET_DEFAULT: kwargs["recursive"] = recursive self.__wrapped__.rmdir(**kwargs) def rename( self, target: WritablePathLike, *, # note: non-standard compared to pathlib recursive: bool = UNSET_DEFAULT, maxdepth: int | None = UNSET_DEFAULT, **kwargs: Any, ) -> Self: if recursive is not UNSET_DEFAULT: kwargs["recursive"] = recursive if maxdepth is not UNSET_DEFAULT: kwargs["maxdepth"] = maxdepth return self._from_upath( self.__wrapped__.rename( target.__wrapped__ if isinstance(target, ProxyUPath) else target, **kwargs, ) ) def replace(self, target: WritablePathLike) -> Self: return self._from_upath(self.__wrapped__.replace(target)) @property def drive(self) -> str: return self.__wrapped__.drive @property def root(self) -> str: return self.__wrapped__.root def __reduce__(self): return type(self)._from_upath, (self.__wrapped__,) @classmethod def from_uri(cls, uri: str, **storage_options: Any) -> Self: return cls(uri, **storage_options) def as_uri(self) -> str: return self.__wrapped__.as_uri() def as_posix(self) -> str: return self.__wrapped__.as_posix() def samefile(self, other_path) -> bool: return self.__wrapped__.samefile(other_path) @classmethod_or_method def cwd(cls_or_self) -> Self: # noqa: B902 if isinstance(cls_or_self, type): raise UnsupportedOperation(".cwd() not supported") else: return cls_or_self._from_upath(cls_or_self.__wrapped__.cwd()) @classmethod def home(cls) -> Self: raise UnsupportedOperation(".home() not supported") def relative_to( # type: ignore[override] self, other, /, *_deprecated, walk_up=False, ) -> Self: if isinstance(other, ProxyUPath): other = other.__wrapped__ return self._from_upath( self.__wrapped__.relative_to(other, *_deprecated, walk_up=walk_up) ) def is_relative_to(self, other, /, *_deprecated) -> bool: # type: ignore[override] if not isinstance(other, str): other = vfspath(other) return self.__wrapped__.is_relative_to(other, *_deprecated) def hardlink_to(self, target: ReadablePathLike) -> None: if not isinstance(target, str): target = vfspath(target) return self.__wrapped__.hardlink_to(target) def match(self, pattern: str, *, case_sensitive: bool | None = None) -> bool: return self.__wrapped__.match(pattern) @property def protocol(self) -> str: return self.__wrapped__.protocol @property def storage_options(self) -> Mapping[str, Any]: return self.__wrapped__.storage_options @property def fs(self) -> AbstractFileSystem: return self.__wrapped__.fs @property def path(self) -> str: return self.__wrapped__.path def joinuri(self, uri: JoinablePathLike) -> Self: return self._from_upath(self.__wrapped__.joinuri(uri)) @property def _url(self) -> SplitResult: return self.__wrapped__._url def read_bytes(self) -> bytes: return self.__wrapped__.read_bytes() def read_text( self, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> str: return self.__wrapped__.read_text( encoding=encoding, errors=errors, newline=newline ) def walk( self, top_down: bool = True, on_error: Callable[[Exception], Any] | None = None, follow_symlinks: bool = False, ) -> Iterator[tuple[Self, list[str], list[str]]]: for pth, dirnames, filenames in self.__wrapped__.walk( top_down=top_down, on_error=on_error, follow_symlinks=follow_symlinks ): yield self._from_upath(pth), dirnames, filenames def copy(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.copy(target, **kwargs)) def copy_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.copy_into(target_dir, **kwargs)) def move(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.move(target, **kwargs)) def move_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.move_into(target_dir, **kwargs)) def write_bytes(self, data: bytes) -> int: return self.__wrapped__.write_bytes(data) def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: return self.__wrapped__.write_text( data, encoding=encoding, errors=errors, newline=newline ) def _copy_from( self, source: ReadablePath | Self, follow_symlinks: bool = True, on_name_collision: OnNameCollisionFunc | None = None, **kwargs: Any, ) -> None: self.__wrapped__._copy_from(source, follow_symlinks=follow_symlinks, on_name_collision=on_name_collision, **kwargs) # type: ignore # noqa: E501 @property def anchor(self) -> str: return self.__wrapped__.anchor @property def name(self) -> str: return self.__wrapped__.name @property def suffix(self) -> str: return self.__wrapped__.suffix @property def suffixes(self) -> list[str]: return self.__wrapped__.suffixes @property def stem(self) -> str: return self.__wrapped__.stem def with_stem(self, stem: str) -> Self: return self._from_upath(self.__wrapped__.with_stem(stem)) def with_suffix(self, suffix: str) -> Self: return self._from_upath(self.__wrapped__.with_suffix(suffix)) def joinpath(self, *pathsegments: JoinablePathLike) -> Self: return self._from_upath(self.__wrapped__.joinpath(*pathsegments)) def __truediv__(self, other: str | Self) -> Self: return self._from_upath( self.__wrapped__.__truediv__(other) # type: ignore[operator] ) def __rtruediv__(self, other: str | Self) -> Self: return self._from_upath( self.__wrapped__.__rtruediv__(other) # type: ignore[operator] ) @property def parent(self) -> Self: return self._from_upath(self.__wrapped__.parent) @property def parents(self) -> Sequence[Self]: return tuple(self._from_upath(p) for p in self.__wrapped__.parents) def full_match( self, pattern: str | SupportsPathLike, *, case_sensitive: bool | None = None, ) -> bool: return self.__wrapped__.full_match(pattern, case_sensitive=case_sensitive) @classmethod def __get_pydantic_core_schema__( cls, source_type: Any, handler: GetCoreSchemaHandler ) -> CoreSchema: cs = UPath.__get_pydantic_core_schema__.__func__ # type: ignore[attr-defined] return cs(cls, source_type, handler) UPath.register(ProxyUPath) universal_pathlib-0.3.10/upath/implementations/000077500000000000000000000000001514661127100216435ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/implementations/__init__.py000066400000000000000000000000001514661127100237420ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/implementations/_experimental.py000066400000000000000000000014161514661127100250530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from upath.registry import get_upath_class if TYPE_CHECKING: from upath import UPath def __getattr__(name: str) -> type[UPath]: if name.startswith("_") and name.endswith("Path"): from upath import UPath protocol = name[1:-4].lower() cls = get_upath_class(protocol) if cls is None: raise RuntimeError( f"Could not find fsspec implementation for protocol {protocol!r}" ) elif not issubclass(cls, UPath): raise RuntimeError( "UPath implementation not a subclass of upath.UPath, {cls!r}" ) return cls raise AttributeError(f"module {__name__!r} has no attribute {name!r}") universal_pathlib-0.3.10/upath/implementations/cached.py000066400000000000000000000027121514661127100234260ustar00rootroot00000000000000from __future__ import annotations import sys from types import MappingProxyType from typing import TYPE_CHECKING from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from collections.abc import Mapping from typing import Any from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from fsspec import AbstractFileSystem from upath._chain import FSSpecChainParser from upath.types.storage_options import SimpleCacheStorageOptions __all__ = ["SimpleCachePath"] class SimpleCachePath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["simplecache"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[SimpleCacheStorageOptions], ) -> None: ... @classmethod def _fs_factory( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any], ) -> AbstractFileSystem: so = dict(storage_options) so.pop("fo", None) return super()._fs_factory( urlpath, protocol, so, ) @property def storage_options(self) -> Mapping[str, Any]: so = self._storage_options.copy() so.pop("fo", None) return MappingProxyType(so) universal_pathlib-0.3.10/upath/implementations/cloud.py000066400000000000000000000172111514661127100233250ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Iterator from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any from typing import overload from upath import UnsupportedOperation from upath._chain import DEFAULT_CHAIN_PARSER from upath._flavour import upath_strip_protocol from upath.core import UPath from upath.types import JoinablePathLike from upath.types import SupportsPathLike from upath.types import WritablePath if TYPE_CHECKING: from typing import Literal from typing import TypeVar if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import AzureStorageOptions from upath.types.storage_options import GCSStorageOptions from upath.types.storage_options import HfStorageOptions from upath.types.storage_options import S3StorageOptions _WT = TypeVar("_WT", bound="WritablePath") __all__ = [ "CloudPath", "GCSPath", "S3Path", "AzurePath", "HfPath", ] class CloudPath(UPath): __slots__ = () @classmethod def _transform_init_args( cls, args: tuple[JoinablePathLike, ...], protocol: str, storage_options: dict[str, Any], ) -> tuple[tuple[JoinablePathLike, ...], str, dict[str, Any]]: for key in ["bucket", "netloc"]: bucket = storage_options.pop(key, None) if bucket: if str(args[0]).startswith("/"): args = (f"{protocol}://{bucket}{args[0]}", *args[1:]) else: args0 = upath_strip_protocol(args[0]) args = (f"{protocol}://{bucket}/", args0, *args[1:]) break return super()._transform_init_args(args, protocol, storage_options) @property def root(self) -> str: if self._relative_base is not None: return "" return self.parser.sep def __str__(self) -> str: path = super().__str__() if self._relative_base is None: drive = self.parser.splitdrive(path)[0] if drive and path == f"{self.protocol}://{drive}": return f"{path}{self.root}" return path @property def path(self) -> str: self_path = super().path.rstrip(self.parser.sep) if ( self._relative_base is None and self_path and self.parser.sep not in self_path ): return self_path + self.root return self_path @property def parts(self) -> Sequence[str]: parts = super().parts if self._relative_base is None and len(parts) == 2 and not parts[1]: return parts[:1] return parts def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False ) -> None: if not parents and not exist_ok and self.exists(): raise FileExistsError(self.path) super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok) class GCSPath(CloudPath): __slots__ = () def __init__( self, *args: JoinablePathLike, protocol: Literal["gcs", "gs"] | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Unpack[GCSStorageOptions], ) -> None: super().__init__( *args, protocol=protocol, chain_parser=chain_parser, **storage_options ) if not self.drive and len(self.parts) > 1: raise ValueError("non key-like path provided (bucket/container missing)") def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False ) -> None: try: super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok) except TypeError as err: if "unexpected keyword argument 'create_parents'" in str(err): self.fs.mkdir(self.path) def exists(self, *, follow_symlinks: bool = True) -> bool: # required for gcsfs<2025.5.0, see: https://github.com/fsspec/gcsfs/pull/676 path = self.path if len(path) > 1: path = path.removesuffix(self.root) return self.fs.exists(path) class S3Path(CloudPath): __slots__ = () def __init__( self, *args: JoinablePathLike, protocol: Literal["s3", "s3a"] | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Unpack[S3StorageOptions], ) -> None: super().__init__( *args, protocol=protocol, chain_parser=chain_parser, **storage_options ) if not self.drive and len(self.parts) > 1: raise ValueError("non key-like path provided (bucket/container missing)") @overload def copy(self, target: _WT, **kwargs: Any) -> _WT: ... @overload def copy(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... def copy(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath: """ Recursively copy this file or directory tree to the given destination. """ # to allow _copy_from to check if a path isfile AND isdir # we need to disable s3fs's dircache mechanism because it # currently implements a XOR relation the two for objects # ref: fsspec/s3fs#999 sopts = dict(self.storage_options) sopts["use_listings_cache"] = False new_self = type(self)( self.path, protocol=self.protocol, # type: ignore **sopts, ) assert type(self) is type(new_self) return super(type(new_self), new_self).copy(target, **kwargs) class AzurePath(CloudPath): __slots__ = () def __init__( self, *args: JoinablePathLike, protocol: Literal["abfs", "abfss", "adl", "az"] | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Unpack[AzureStorageOptions], ) -> None: super().__init__( *args, protocol=protocol, chain_parser=chain_parser, **storage_options ) if not self.drive and len(self.parts) > 1: raise ValueError("non key-like path provided (bucket/container missing)") class HfPath(CloudPath): __slots__ = () def __init__( self, *args: JoinablePathLike, protocol: Literal["hf"] | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Unpack[HfStorageOptions], ) -> None: super().__init__( *args, protocol=protocol, chain_parser=chain_parser, **storage_options ) @property def root(self) -> str: return "" def iterdir(self) -> Iterator[Self]: try: yield from super().iterdir() except NotImplementedError: raise UnsupportedOperation def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: raise UnsupportedOperation def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: raise UnsupportedOperation def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation("DataPath does not support writing") def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("DataPath does not support writing") universal_pathlib-0.3.10/upath/implementations/data.py000066400000000000000000000067441514661127100231410ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Iterator from collections.abc import Sequence from typing import TYPE_CHECKING from urllib.parse import quote_plus from upath._protocol import get_upath_protocol from upath.core import UnsupportedOperation from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import DataStorageOptions __all__ = ["DataPath"] class DataPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["data"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[DataStorageOptions], ) -> None: ... @property def parts(self) -> Sequence[str]: return (self.path,) def __str__(self) -> str: return self.parser.join(*self._raw_urlpaths) def with_segments(self, *pathsegments: JoinablePathLike) -> Self: try: (segment,) = pathsegments except ValueError: raise UnsupportedOperation("join not supported by DataPath") if get_upath_protocol(segment) != "data": raise ValueError(f"requires a data URI, got: {segment!r}") return type(self)(segment, protocol="data", **self.storage_options) @property def name(self) -> str: return quote_plus(self.path) @property def stem(self) -> str: return quote_plus(self.path) @property def suffix(self) -> str: return "" @property def suffixes(self) -> list[str]: return [] def with_name(self, name: str) -> Self: raise UnsupportedOperation("with_name not supported by DataPath") def with_suffix(self, suffix: str) -> Self: raise UnsupportedOperation("with_suffix not supported by DataPath") def with_stem(self, stem: str) -> Self: raise UnsupportedOperation("with_stem not supported by DataPath") @property def parent(self) -> Self: return self @property def parents(self) -> Sequence[Self]: return [] def full_match(self, pattern, *, case_sensitive: bool | None = None) -> bool: return super().full_match(pattern, case_sensitive=case_sensitive) def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False ) -> None: raise FileExistsError(str(self)) def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation("DataPath does not support writing") def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("DataPath does not support writing") def iterdir(self) -> Iterator[Self]: raise NotADirectoryError def glob( self, pattern, *, case_sensitive=None, recurse_symlinks=False ) -> Iterator[Self]: return iter([]) def rglob( self, pattern, *, case_sensitive=None, recurse_symlinks=False ) -> Iterator[Self]: return iter([]) def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation universal_pathlib-0.3.10/upath/implementations/ftp.py000066400000000000000000000035261514661127100230140ustar00rootroot00000000000000from __future__ import annotations import sys from ftplib import error_perm as FTPPermanentError # nosec B402 from typing import TYPE_CHECKING from upath.core import UPath from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Any from typing import Literal if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types import WritablePathLike from upath.types.storage_options import FTPStorageOptions __all__ = ["FTPPath"] class FTPPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["ftp"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[FTPStorageOptions], ) -> None: ... def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: try: return super().mkdir(mode, parents, exist_ok) except FTPPermanentError as e: if e.args[0].startswith("550") and exist_ok: return raise FileExistsError(str(self)) from e def rename( self, target: WritablePathLike, *, # note: non-standard compared to pathlib recursive: bool = UNSET_DEFAULT, maxdepth: int | None = UNSET_DEFAULT, **kwargs: Any, ) -> Self: t = super().rename(target, recursive=recursive, maxdepth=maxdepth, **kwargs) self_dir = self.parent.path t.fs.invalidate_cache(self_dir) self.fs.invalidate_cache(self_dir) return t universal_pathlib-0.3.10/upath/implementations/github.py000066400000000000000000000037451514661127100235100ustar00rootroot00000000000000""" GitHub file system implementation """ from __future__ import annotations import sys from collections.abc import Sequence from typing import TYPE_CHECKING from upath.core import UnsupportedOperation from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import GitHubStorageOptions __all__ = ["GitHubPath"] class GitHubPath(UPath): """ GitHubPath supporting the fsspec.GitHubFileSystem """ __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["github"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[GitHubStorageOptions], ) -> None: ... @property def path(self) -> str: pth = super().path if pth == ".": return "" return pth @property def parts(self) -> Sequence[str]: parts = super().parts if parts and parts[0] == "/": return parts[1:] else: return parts def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: raise UnsupportedOperation def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: raise UnsupportedOperation def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("GitHubPath does not support writing") universal_pathlib-0.3.10/upath/implementations/hdfs.py000066400000000000000000000020431514661127100231400ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import HDFSStorageOptions __all__ = ["HDFSPath"] class HDFSPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["hdfs"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[HDFSStorageOptions], ) -> None: ... def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False ) -> None: if not exist_ok and self.exists(): raise FileExistsError(str(self)) super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok) universal_pathlib-0.3.10/upath/implementations/http.py000066400000000000000000000112701514661127100231750ustar00rootroot00000000000000from __future__ import annotations import sys import warnings from collections.abc import Iterator from itertools import chain from typing import TYPE_CHECKING from typing import Any from urllib.parse import urlsplit from fsspec.asyn import sync from upath import UnsupportedOperation from upath._stat import UPathStatResult from upath.core import UPath from upath.types import JoinablePathLike from upath.types import StatResultType if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import HTTPStorageOptions __all__ = ["HTTPPath"] class HTTPPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["http", "https"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[HTTPStorageOptions], ) -> None: ... @classmethod def _transform_init_args( cls, args: tuple[JoinablePathLike, ...], protocol: str, storage_options: dict[str, Any], ) -> tuple[tuple[JoinablePathLike, ...], str, dict[str, Any]]: # allow initialization via a path argument and protocol keyword if args and not str(args[0]).startswith(protocol): args = (f"{protocol}://{str(args[0]).lstrip('/')}", *args[1:]) return args, protocol, storage_options def __str__(self) -> str: sr = urlsplit(super().__str__()) return sr._replace(path=sr.path or "/").geturl() @property def path(self) -> str: sr = urlsplit(super().path) return sr._replace(path=sr.path or "/").geturl() def stat(self, follow_symlinks: bool = True) -> StatResultType: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.stat(follow_symlinks=False):" " is currently ignored.", UserWarning, stacklevel=2, ) info = self.fs.info(self.path) if "url" in info: info["type"] = "directory" if info["url"].endswith("/") else "file" return UPathStatResult.from_info(info) def iterdir(self) -> Iterator[Self]: it = iter(super().iterdir()) try: item0 = next(it) except (StopIteration, NotADirectoryError): raise NotADirectoryError(str(self)) except FileNotFoundError: raise FileNotFoundError(str(self)) else: yield from chain([item0], it) def resolve( self, strict: bool = False, follow_redirects: bool = True, ) -> Self: """Normalize the path and resolve redirects.""" # special handling of trailing slash behaviour parts = list(self.parts) if parts[-1:] == ["."]: parts[-1:] = [""] if parts[-2:] == ["", ".."]: parts[-2:] = [""] pth = self.with_segments(*parts) resolved_path = super(HTTPPath, pth).resolve(strict=strict) if follow_redirects: cls = type(self) # Get the fsspec fs fs = self.fs url = str(self) # Ensure we have a session session = sync(fs.loop, fs.set_session) # Use HEAD requests if the server allows it, falling back to GETs for method in (session.head, session.get): r = sync(fs.loop, method, url, allow_redirects=True) try: r.raise_for_status() except Exception as exc: if method == session.get: raise FileNotFoundError(self) from exc else: resolved_path = cls(str(r.url)) break return resolved_path def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: raise UnsupportedOperation def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: raise UnsupportedOperation def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation("DataPath does not support writing") def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("DataPath does not support writing") universal_pathlib-0.3.10/upath/implementations/local.py000066400000000000000000000647021514661127100233200ustar00rootroot00000000000000from __future__ import annotations import os import pathlib import shutil import sys import warnings from collections.abc import Iterator from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Literal from typing import overload from urllib.parse import SplitResult from fsspec import AbstractFileSystem from upath._chain import DEFAULT_CHAIN_PARSER from upath._chain import Chain from upath._chain import ChainSegment from upath._chain import FSSpecChainParser from upath._protocol import compatible_protocol from upath._protocol import get_upath_protocol from upath.core import UnsupportedOperation from upath.core import UPath from upath.core import _UPathMixin from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike from upath.types import PathInfo from upath.types import ReadablePath from upath.types import ReadablePathLike from upath.types import StatResultType from upath.types import SupportsPathLike from upath.types import WritablePath from upath.types import WritablePathLike if TYPE_CHECKING: from typing import IO from typing import BinaryIO from typing import TextIO from typing import TypeVar if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from pydantic import GetCoreSchemaHandler from pydantic_core.core_schema import CoreSchema from upath.types.storage_options import FileStorageOptions _WT = TypeVar("_WT", bound="WritablePath") __all__ = [ "LocalPath", "PosixUPath", "WindowsUPath", "FilePath", ] _LISTDIR_WORKS_ON_FILES: bool | None = None def _check_listdir_works_on_files() -> bool: global _LISTDIR_WORKS_ON_FILES from fsspec.implementations.local import LocalFileSystem fs = LocalFileSystem() try: fs.ls(__file__) except NotADirectoryError: _LISTDIR_WORKS_ON_FILES = w = False else: _LISTDIR_WORKS_ON_FILES = w = True return w def _warn_protocol_storage_options( cls: type, protocol: str | None, storage_options: dict[str, Any], ) -> None: if protocol in {"", None} and not storage_options: return warnings.warn( f"{cls.__name__} on python <= (3, 11) ignores protocol and storage_options", UserWarning, stacklevel=3, ) class _LocalPathInfo(PathInfo): """Backported PathInfo implementation for LocalPath. todo: currently not handling symlinks correctly. """ def __init__(self, path: LocalPath) -> None: self._path = path.path def exists(self, *, follow_symlinks: bool = True) -> bool: return os.path.exists(self._path) def is_dir(self, *, follow_symlinks: bool = True) -> bool: return os.path.isdir(self._path) def is_file(self, *, follow_symlinks: bool = True) -> bool: return os.path.isfile(self._path) def is_symlink(self) -> bool: return os.path.islink(self._path) class LocalPath(_UPathMixin, pathlib.Path): __slots__ = ( "_chain", "_chain_parser", "_fs_cached", "_relative_base", ) if TYPE_CHECKING: _chain: Chain _chain_parser: FSSpecChainParser _fs_cached: AbstractFileSystem _relative_base: str | None parser = os.path # type: ignore[misc,assignment] @property def _raw_urlpaths(self) -> Sequence[JoinablePathLike]: return self.parts @_raw_urlpaths.setter def _raw_urlpaths(self, value: Sequence[JoinablePathLike]) -> None: pass if sys.version_info >= (3, 14): def rename( self, target: WritablePathLike, ) -> Self: t = super().rename(target) # type: ignore[arg-type] if not isinstance(target, type(self)): return self.with_segments(t) else: return t if sys.version_info >= (3, 12): def __init__( self, *args, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> None: super(_UPathMixin, self).__init__(*args) self._chain = Chain(ChainSegment(str(self), "", storage_options)) self._chain_parser = chain_parser elif sys.version_info >= (3, 10): def __init__( self, *args, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> None: # super(_UPathMixin, self).__init__(*args) _warn_protocol_storage_options(type(self), protocol, storage_options) self._drv, self._root, self._parts = self._parse_args(args) # type: ignore[attr-defined] # noqa: E501 self._chain = Chain(ChainSegment(str(self), "", {})) self._chain_parser = chain_parser @classmethod def _from_parts(cls, args): obj = super()._from_parts(args) obj._chain = Chain(ChainSegment(str(obj), "", {})) return obj @classmethod def _from_parsed_parts(cls, drv, root, parts): obj = super()._from_parsed_parts(drv, root, parts) obj._chain = Chain(ChainSegment(str(obj), "", {})) return obj else: def __init__( self, *args, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> None: _warn_protocol_storage_options(type(self), protocol, storage_options) self._drv, self._root, self._parts = self._parse_args(args) # type: ignore[attr-defined] # noqa: E501 self._init() self._chain_parser = chain_parser def _init(self, **kwargs: Any) -> None: super()._init(**kwargs) # type: ignore[misc] self._chain = Chain(ChainSegment(str(self), "", {})) def __vfspath__(self) -> str: return self.__fspath__() def __open_reader__(self) -> BinaryIO: return self.open("rb") def __eq__(self, other: object) -> bool: if not isinstance(other, UPath): return NotImplemented eq_path = super().__eq__(other) if eq_path is NotImplemented: return NotImplemented return ( eq_path and self.protocol == other.protocol and self.storage_options == other.storage_options ) def __ne__(self, other: object) -> bool: if not isinstance(other, UPath): return NotImplemented ne_path = super().__ne__(other) if ne_path is NotImplemented: return NotImplemented return ( ne_path or self.protocol != other.protocol or self.storage_options != other.storage_options ) def __hash__(self) -> int: return super().__hash__() if sys.version_info >= (3, 14): def __open_rb__(self, buffering: int = UNSET_DEFAULT) -> BinaryIO: return self.open("rb", buffering=buffering) def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: if mode == "w": return self.open(mode="wb") elif mode == "a": return self.open(mode="ab") elif mode == "x": return self.open(mode="xb") else: raise ValueError(f"invalid mode: {mode}") def with_segments(self, *pathsegments: str | os.PathLike[str]) -> Self: return type(self)( *pathsegments, protocol=self._protocol, **self._storage_options, ) @property def path(self) -> str: return self.as_posix() @property def _url(self) -> SplitResult: return SplitResult._make((self.protocol, "", self.path, "", "")) def joinpath(self, *other) -> Self: if not compatible_protocol("", *other): raise ValueError("can't combine incompatible UPath protocols") return super().joinpath( *( str(o) if isinstance(o, UPath) and not o.is_absolute() else o for o in other ) ) def __truediv__(self, other) -> Self: if not compatible_protocol("", other): raise ValueError("can't combine incompatible UPath protocols") return super().__truediv__( str(other) if isinstance(other, UPath) and not other.is_absolute() else other ) def __rtruediv__(self, other) -> Self: if not compatible_protocol("", other): raise ValueError("can't combine incompatible UPath protocols") return super().__rtruediv__( str(other) if isinstance(other, UPath) and not other.is_absolute() else other ) @overload # type: ignore[override] def open( self, mode: Literal["r", "w", "a"] = "r", buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> TextIO: ... @overload def open( self, mode: Literal["rb", "wb", "ab", "xb"], buffering: int = ..., encoding: str = ..., errors: str = ..., newline: str = ..., **fsspec_kwargs: Any, ) -> BinaryIO: ... @overload def open( self, mode: str, buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., **fsspec_kwargs: Any, ) -> IO[Any]: ... def open( self, mode: str = "r", buffering: int = UNSET_DEFAULT, encoding: str | None = UNSET_DEFAULT, errors: str | None = UNSET_DEFAULT, newline: str | None = UNSET_DEFAULT, **fsspec_kwargs: Any, ) -> IO[Any]: if not fsspec_kwargs: kwargs: dict[str, str | int | None] = {} if buffering is not UNSET_DEFAULT: kwargs["buffering"] = buffering if encoding is not UNSET_DEFAULT: kwargs["encoding"] = encoding if errors is not UNSET_DEFAULT: kwargs["errors"] = errors if newline is not UNSET_DEFAULT: kwargs["newline"] = newline return super().open(mode, **kwargs) # type: ignore # noqa: E501 return UPath.open.__get__(self)( mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline, **fsspec_kwargs, ) def rmdir(self, recursive: bool = UNSET_DEFAULT) -> None: if recursive is UNSET_DEFAULT or not recursive: return super().rmdir() else: shutil.rmtree(self) # we need to override pathlib.Path._copy_from to support it as a # WritablePath._copy_from target with support for on_name_collision # Issue: https://github.com/barneygale/pathlib-abc/issues/48 _copy_from = UPath._copy_from if sys.version_info < (3, 14): # noqa: C901 @overload def copy(self, target: _WT, **kwargs: Any) -> _WT: ... @overload def copy(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... def copy( self, target: _WT | SupportsPathLike | str, **kwargs: Any ) -> _WT | Self: # hacky workaround for missing pathlib.Path.copy in python < 3.14 # todo: revisit _copy: Any = ReadablePath.copy.__get__(self) if isinstance(target, str): proto = get_upath_protocol(target) if proto != self.protocol: target = UPath(target) else: target = self.with_segments(target) elif not isinstance(target, UPath): target = UPath(target) if target.is_dir(): raise IsADirectoryError(str(target)) return _copy(target, **kwargs) @overload def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @overload def copy_into( self, target_dir: SupportsPathLike | str, **kwargs: Any ) -> Self: ... def copy_into( self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any, ) -> _WT | Self: # hacky workaround for missing pathlib.Path.copy_into in python < 3.14 # todo: revisit _copy_into: Any = ReadablePath.copy_into.__get__(self) if isinstance(target_dir, str): proto = get_upath_protocol(target_dir) if proto != self.protocol: target_dir = UPath(target_dir) else: target_dir = self.with_segments(target_dir) elif not isinstance(target_dir, UPath): target_dir = UPath(target_dir) return _copy_into(target_dir, **kwargs) @overload def move(self, target: _WT, **kwargs: Any) -> _WT: ... @overload def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... def move( self, target: _WT | SupportsPathLike | str, **kwargs: Any ) -> _WT | Self: target = self.copy(target, **kwargs) self.fs.rm(self.path, recursive=self.is_dir()) return target @overload def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @overload def move_into( self, target_dir: SupportsPathLike | str, **kwargs: Any ) -> Self: ... def move_into( self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any ) -> _WT | Self: name = self.name if not name: raise ValueError(f"{self!r} has an empty name") elif hasattr(target_dir, "with_segments"): target = target_dir.with_segments(str(target_dir), name) # type: ignore elif isinstance(target_dir, pathlib.PurePath): target = UPath(target_dir, name) else: target = self.with_segments(str(target_dir), name) td = target.parent if not td.exists(): raise FileNotFoundError(str(td)) elif not td.is_dir(): raise NotADirectoryError(str(td)) return self.move(target) @property def info(self) -> PathInfo: return _LocalPathInfo(self) if sys.version_info < (3, 13): # noqa: C901 def full_match( self, pattern: str | os.PathLike[str], *, case_sensitive: bool | None = None, ) -> bool: # hacky workaround for missing pathlib.Path.full_match in python < 3.13 # todo: revisit return self.match( pattern, # type: ignore[arg-type] case_sensitive=case_sensitive, ) @classmethod def from_uri(cls, uri: str, **storage_options: Any) -> Self: return UPath(uri, **storage_options) # type: ignore[return-value] def is_dir(self, *, follow_symlinks: bool = True) -> bool: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.is_dir(): follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().is_dir() def is_file(self, *, follow_symlinks: bool = True) -> bool: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.is_file(): follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().is_file() def read_text( self, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> str: if newline is not None: warnings.warn( f"{type(self).__name__}.read_text(): newline" " is currently ignored.", UserWarning, stacklevel=2, ) return super().read_text( encoding=encoding, errors=errors, ) def glob( # type: ignore[override] self, pattern: str | os.PathLike, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: if isinstance(pattern, str): pattern_str = pattern else: pattern_str = os.fspath(pattern) kw: dict[str, Any] = {} if case_sensitive is not None: if sys.version_info < (3, 12): warnings.warn( f"{type(self).__name__}.glob(): case_sensitive" " is currently ignored.", UserWarning, stacklevel=2, ) else: kw["case_sensitive"] = case_sensitive if recurse_symlinks: warnings.warn( f"{type(self).__name__}.glob(): recurse_symlinks=True" " is currently ignored.", UserWarning, stacklevel=2, ) return super().glob(pattern_str, **kw) def rglob( # type: ignore[override] self, pattern: str | os.PathLike, *, case_sensitive: bool | None = None, recurse_symlinks: bool = False, ) -> Iterator[Self]: if isinstance(pattern, str): pattern_str = pattern else: pattern_str = os.fspath(pattern) kw: dict[str, Any] = {} if case_sensitive is not None: if sys.version_info < (3, 12): warnings.warn( f"{type(self).__name__}.rglob(): case_sensitive" " is currently ignored.", UserWarning, stacklevel=2, ) else: kw["case_sensitive"] = case_sensitive if recurse_symlinks: warnings.warn( f"{type(self).__name__}.rglob(): recurse_symlinks=True" " is currently ignored.", UserWarning, stacklevel=2, ) return super().rglob(pattern_str, **kw) def owner(self, *, follow_symlinks: bool = True) -> str: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.owner() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().owner() def group(self, *, follow_symlinks: bool = True) -> str: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.group() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().group() if sys.version_info < (3, 12): def is_junction(self) -> bool: return False def walk( self, top_down: bool = True, on_error: Callable[[Exception], Any] | None = None, follow_symlinks: bool = False, ) -> Iterator[tuple[Self, list[str], list[str]]]: _walk = ReadablePath.walk.__get__(self) return _walk(top_down, on_error, follow_symlinks) def match( self, path_pattern: str | os.PathLike[str], *, case_sensitive: bool | None = None, ) -> bool: if isinstance(path_pattern, str): pattern_str = path_pattern else: pattern_str = os.fspath(path_pattern) if case_sensitive is not None: warnings.warn( f"{type(self).__name__}.match(): case_sensitive" " is currently ignored.", UserWarning, stacklevel=2, ) return super().match(pattern_str) def exists(self, *, follow_symlinks: bool = True) -> bool: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.exists(): follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().exists() def relative_to( # type: ignore[override] self, other: str | os.PathLike[str], /, *_deprecated: str | os.PathLike[str], walk_up: bool = False, ) -> Self: if walk_up: warnings.warn( f"{type(self).__name__}.relative_to() walk_up=True" " is currently ignored.", UserWarning, stacklevel=2, ) return super().relative_to(other, *_deprecated) if sys.version_info < (3, 10): def hardlink_to(self, target: ReadablePathLike) -> None: try: os.link(os.fspath(target), os.fspath(self)) # type: ignore[arg-type] except AttributeError: raise UnsupportedOperation("hardlink operation not supported") # let's skip this one as backporting it breaks one pathlib test # @property # def parents(self) -> Sequence[Self]: # return list(super().parents) def stat( # type: ignore[override] self, *, follow_symlinks: bool = True, ) -> StatResultType: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.stat() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().stat() # type: ignore[return-value] def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: if newline is not None: warnings.warn( f"{type(self).__name__}.write_text() newline" " is currently ignored.", UserWarning, stacklevel=2, ) return super().write_text( data, encoding=encoding, errors=errors, ) def chmod( self, mode: int, *, follow_symlinks: bool = True, ) -> None: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.chmod() follow_symlinks=False" " is currently ignored.", UserWarning, stacklevel=2, ) return super().chmod(mode) @classmethod def __get_pydantic_core_schema__( cls, source_type: Any, handler: GetCoreSchemaHandler ) -> CoreSchema: cs = UPath.__get_pydantic_core_schema__.__func__ # type: ignore[attr-defined] return cs(cls, source_type, handler) UPath.register(LocalPath) # Mypy will ignore the ABC.register call above, so we need to force it to # think PosixUPath and WindowsUPath are subclasses of UPath. # This is really not a good pattern, but it's the best we can do without # either introducing a duck-type protocol for UPath or come up with a # better solution for the UPath versions of the pathlib.Path subclasses. if TYPE_CHECKING: class WindowsUPath(LocalPath, pathlib.WindowsPath, UPath): # type: ignore[misc] __slots__ = () class PosixUPath(LocalPath, pathlib.PosixPath, UPath): # type: ignore[misc] __slots__ = () else: class WindowsUPath(LocalPath, pathlib.WindowsPath): __slots__ = () if os.name != "nt": def __new__( cls, *args, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> WindowsUPath: raise UnsupportedOperation( f"cannot instantiate {cls.__name__} on your system" ) class PosixUPath(LocalPath, pathlib.PosixPath): __slots__ = () if os.name == "nt": def __new__( cls, *args, protocol: str | None = None, chain_parser: FSSpecChainParser = DEFAULT_CHAIN_PARSER, **storage_options: Any, ) -> PosixUPath: raise UnsupportedOperation( f"cannot instantiate {cls.__name__} on your system" ) class FilePath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["file", "local"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[FileStorageOptions], ) -> None: ... def __fspath__(self) -> str: return self.path def iterdir(self) -> Iterator[Self]: if _LISTDIR_WORKS_ON_FILES is None: _check_listdir_works_on_files() elif _LISTDIR_WORKS_ON_FILES and self.is_file(): raise NotADirectoryError(f"{self}") return super().iterdir() @property def _url(self) -> SplitResult: return SplitResult._make((self.protocol, "", self.path, "", "")) @classmethod def cwd(cls) -> Self: return cls(os.getcwd(), protocol="file") @classmethod def home(cls) -> Self: return cls(os.path.expanduser("~"), protocol="file") def chmod( self, mode: int, *, follow_symlinks: bool = True, ) -> None: return os.chmod(self.path, mode, follow_symlinks=follow_symlinks) LocalPath.register(FilePath) universal_pathlib-0.3.10/upath/implementations/memory.py000066400000000000000000000023561514661127100235330ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import MemoryStorageOptions __all__ = ["MemoryPath"] class MemoryPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["memory"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[MemoryStorageOptions], ) -> None: ... @property def path(self) -> str: path = super().path return "/" if path in {"", "."} else path def is_absolute(self) -> bool: if self._relative_base is None and self.__vfspath__() == "/": return True return super().is_absolute() def __str__(self) -> str: s = super().__str__() if s.startswith("memory:///"): s = s.replace("memory:///", "memory://", 1) return s universal_pathlib-0.3.10/upath/implementations/sftp.py000066400000000000000000000021711514661127100231720ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import SFTPStorageOptions __all__ = ["SFTPPath"] class SFTPPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["sftp"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[SFTPStorageOptions], ) -> None: ... @property def path(self) -> str: path = super().path if len(path) > 1: return path.removesuffix("/") return path def __str__(self) -> str: path_str = super().__str__() if path_str.startswith(("ssh:///", "sftp:///")): return path_str.removesuffix("/") return path_str universal_pathlib-0.3.10/upath/implementations/smb.py000066400000000000000000000053131514661127100230000ustar00rootroot00000000000000from __future__ import annotations import sys import warnings from typing import TYPE_CHECKING from typing import Any from upath.core import UPath from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike from upath.types import WritablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import SMBStorageOptions class SMBPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["smb"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[SMBStorageOptions], ) -> None: ... @property def path(self) -> str: path = super().path if len(path) > 1: return path.removesuffix("/") # At root level, return "/" to match anchor if not path and self._relative_base is None: return self.anchor return path def __str__(self) -> str: path_str = super().__str__() if path_str.startswith("smb:///"): return path_str.removesuffix("/") return path_str def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: # smbclient does not support setting mode externally from smbprotocol.exceptions import SMBOSError if parents and not exist_ok and self.exists(): raise FileExistsError(str(self)) try: self.fs.mkdir( self.path, create_parents=parents, ) except SMBOSError: if not exist_ok: raise FileExistsError(str(self)) if not self.is_dir(): raise FileExistsError(str(self)) def rename( self, target: WritablePathLike, *, recursive: bool = UNSET_DEFAULT, maxdepth: int | None = UNSET_DEFAULT, **kwargs: Any, ) -> Self: if recursive is not UNSET_DEFAULT: warnings.warn( "SMBPath.rename(): recursive is currently ignored.", UserWarning, stacklevel=2, ) if maxdepth is not UNSET_DEFAULT: warnings.warn( "SMBPath.rename(): maxdepth is currently ignored.", UserWarning, stacklevel=2, ) return super().rename(target, **kwargs) universal_pathlib-0.3.10/upath/implementations/tar.py000066400000000000000000000050261514661127100230060ustar00rootroot00000000000000from __future__ import annotations import stat import sys import warnings from typing import TYPE_CHECKING from upath._stat import UPathStatResult from upath.core import UnsupportedOperation from upath.core import UPath from upath.types import JoinablePathLike from upath.types import StatResultType if TYPE_CHECKING: from collections.abc import Iterator from typing import Literal if sys.version_info >= (3, 11): from typing import Self from typing import Unpack else: from typing_extensions import Self from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import TarStorageOptions __all__ = ["TarPath"] class TarPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["zip"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[TarStorageOptions], ) -> None: ... def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: raise UnsupportedOperation def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: raise UnsupportedOperation def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation("DataPath does not support writing") def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("DataPath does not support writing") def stat( self, *, follow_symlinks: bool = True, ) -> StatResultType: if not follow_symlinks: warnings.warn( f"{type(self).__name__}.stat(follow_symlinks=False):" " is currently ignored.", UserWarning, stacklevel=2, ) info = self.fs.info(self.path).copy() # convert mode if info["type"] == "directory": info["mode"] = stat.S_IFDIR elif info["type"] == "file": info["mode"] = stat.S_IFREG return UPathStatResult.from_info(info) def iterdir(self) -> Iterator[Self]: it = iter(super().iterdir()) p0 = next(it) if p0.name != "": yield p0 yield from it universal_pathlib-0.3.10/upath/implementations/webdav.py000066400000000000000000000055531514661127100234750ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Mapping from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any from urllib.parse import urlsplit from fsspec.registry import known_implementations from fsspec.registry import register_implementation from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import WebdavStorageOptions __all__ = ["WebdavPath"] # webdav was only registered in fsspec>=2022.5.0 if "webdav" not in known_implementations: import webdav4.fsspec register_implementation("webdav", webdav4.fsspec.WebdavFileSystem) class WebdavPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["webdav+http", "webdav+https"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[WebdavStorageOptions], ) -> None: ... @classmethod def _transform_init_args( cls, args: tuple[JoinablePathLike, ...], protocol: str, storage_options: dict[str, Any], ) -> tuple[tuple[JoinablePathLike, ...], str, dict[str, Any]]: if not args: args = ("/",) elif args and protocol in {"webdav+http", "webdav+https"}: args0, *argsN = args url = urlsplit(str(args0)) base = url._replace(scheme=protocol.split("+")[1], path="").geturl() args0 = url._replace(scheme="", netloc="").geturl() or "/" storage_options["base_url"] = base args = (args0, *argsN) if "base_url" not in storage_options: raise ValueError( f"must provide `base_url` storage option for args: {args!r}" ) return super()._transform_init_args(args, "webdav", storage_options) @classmethod def _parse_storage_options( cls, urlpath: str, protocol: str, storage_options: Mapping[str, Any], ) -> dict[str, Any]: so = dict(storage_options) if urlpath.startswith(("webdav+http:", "webdav+https:")): url = urlsplit(str(urlpath)) base = url._replace(scheme=url.scheme.split("+")[1], path="").geturl() urlpath = url._replace(scheme="", netloc="").geturl() or "/" so.setdefault("base_url", base) return super()._parse_storage_options(urlpath, "webdav", so) @property def parts(self) -> Sequence[str]: parts = super().parts if parts and parts[0] == "/": return parts[1:] else: return parts universal_pathlib-0.3.10/upath/implementations/zip.py000066400000000000000000000035701514661127100230240ustar00rootroot00000000000000from __future__ import annotations import sys from typing import TYPE_CHECKING from upath.core import UnsupportedOperation from upath.core import UPath from upath.types import JoinablePathLike if TYPE_CHECKING: from typing import Literal if sys.version_info >= (3, 11): from typing import Unpack else: from typing_extensions import Unpack from upath._chain import FSSpecChainParser from upath.types.storage_options import ZipStorageOptions __all__ = ["ZipPath"] class ZipPath(UPath): __slots__ = () if TYPE_CHECKING: def __init__( self, *args: JoinablePathLike, protocol: Literal["zip"] | None = ..., chain_parser: FSSpecChainParser = ..., **storage_options: Unpack[ZipStorageOptions], ) -> None: ... @classmethod def _transform_init_args(cls, args, protocol, storage_options): if storage_options.get("mode") in {"a", "x", "w"}: raise UnsupportedOperation( "ZipPath write mode disabled in universal-pathlib" ) return super()._transform_init_args(args, protocol, storage_options) def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: raise UnsupportedOperation def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: raise UnsupportedOperation def unlink(self, missing_ok: bool = False) -> None: raise UnsupportedOperation def write_bytes(self, data: bytes) -> int: raise UnsupportedOperation("DataPath does not support writing") def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: raise UnsupportedOperation("DataPath does not support writing") universal_pathlib-0.3.10/upath/py.typed000066400000000000000000000000001514661127100201200ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/registry.py000066400000000000000000000304711514661127100206620ustar00rootroot00000000000000"""upath.registry -- registry for file system specific implementations Retrieve UPath implementations via `get_upath_class`. Register custom UPath subclasses in one of two ways: ### directly from Python >>> from upath import UPath >>> from upath.registry import register_implementation >>> my_protocol = "myproto" >>> class MyPath(UPath): ... pass >>> register_implementation(my_protocol, MyPath) ### via entry points ```toml # pyproject.toml [project.entry-points."universal_pathlib.implementations"] myproto = "my_module.submodule:MyPath" ``` ```ini # setup.cfg [options.entry_points] universal_pathlib.implementations = myproto = my_module.submodule:MyPath ``` """ from __future__ import annotations import os import re import sys import warnings from collections import ChainMap from collections.abc import Iterator from collections.abc import MutableMapping from functools import lru_cache from importlib import import_module from importlib.metadata import entry_points from typing import TYPE_CHECKING from fsspec.core import get_filesystem_class from fsspec.registry import known_implementations as _fsspec_known_implementations from fsspec.registry import registry as _fsspec_registry import upath if TYPE_CHECKING: from typing import Literal from typing import overload from upath.implementations.cached import SimpleCachePath as _SimpleCachePath from upath.implementations.cloud import AzurePath as _AzurePath from upath.implementations.cloud import GCSPath as _GCSPath from upath.implementations.cloud import HfPath as _HfPath from upath.implementations.cloud import S3Path as _S3Path from upath.implementations.data import DataPath as _DataPath from upath.implementations.ftp import FTPPath as _FTPPath from upath.implementations.github import GitHubPath as _GitHubPath from upath.implementations.hdfs import HDFSPath as _HDFSPath from upath.implementations.http import HTTPPath as _HTTPPath from upath.implementations.local import FilePath as _FilePath from upath.implementations.local import PosixUPath as _PosixUPath from upath.implementations.local import WindowsUPath as _WindowsUPath from upath.implementations.memory import MemoryPath as _MemoryPath from upath.implementations.sftp import SFTPPath as _SFTPPath from upath.implementations.smb import SMBPath as _SMBPath from upath.implementations.tar import TarPath as _TarPath from upath.implementations.webdav import WebdavPath as _WebdavPath from upath.implementations.zip import ZipPath as _ZipPath __all__ = [ "get_upath_class", "available_implementations", "register_implementation", ] _ENTRY_POINT_GROUP = "universal_pathlib.implementations" class _Registry(MutableMapping[str, "type[upath.UPath]"]): """internal registry for UPath subclasses""" known_implementations: dict[str, str] = { "abfs": "upath.implementations.cloud.AzurePath", "abfss": "upath.implementations.cloud.AzurePath", "adl": "upath.implementations.cloud.AzurePath", "az": "upath.implementations.cloud.AzurePath", "data": "upath.implementations.data.DataPath", "file": "upath.implementations.local.FilePath", "ftp": "upath.implementations.ftp.FTPPath", "local": "upath.implementations.local.FilePath", "gcs": "upath.implementations.cloud.GCSPath", "gs": "upath.implementations.cloud.GCSPath", "hdfs": "upath.implementations.hdfs.HDFSPath", "hf": "upath.implementations.cloud.HfPath", "http": "upath.implementations.http.HTTPPath", "https": "upath.implementations.http.HTTPPath", "memory": "upath.implementations.memory.MemoryPath", "s3": "upath.implementations.cloud.S3Path", "s3a": "upath.implementations.cloud.S3Path", "simplecache": "upath.implementations.cached.SimpleCachePath", "sftp": "upath.implementations.sftp.SFTPPath", "ssh": "upath.implementations.sftp.SFTPPath", "tar": "upath.implementations.tar.TarPath", "webdav": "upath.implementations.webdav.WebdavPath", "webdav+http": "upath.implementations.webdav.WebdavPath", "webdav+https": "upath.implementations.webdav.WebdavPath", "github": "upath.implementations.github.GitHubPath", "smb": "upath.implementations.smb.SMBPath", "zip": "upath.implementations.zip.ZipPath", } if TYPE_CHECKING: _m: MutableMapping[str, str | type[upath.UPath]] def __init__(self) -> None: if sys.version_info >= (3, 10): eps = entry_points(group=_ENTRY_POINT_GROUP) else: eps = entry_points().get(_ENTRY_POINT_GROUP, []) self._entries = {ep.name: ep for ep in eps} self._m = ChainMap({}, self.known_implementations) # type: ignore def __contains__(self, item: object) -> bool: return item in set().union(self._m, self._entries) def __getitem__(self, item: str) -> type[upath.UPath]: fqn: str | type[upath.UPath] | None = self._m.get(item) if fqn is None: if item in self._entries: fqn = self._m[item] = self._entries[item].load() if fqn is None: raise KeyError(f"{item} not in registry") if isinstance(fqn, str): module_name, name = fqn.rsplit(".", 1) mod = import_module(module_name) cls = getattr(mod, name) # type: ignore else: cls = fqn return cls def __setitem__(self, item: str, value: type[upath.UPath] | str) -> None: if not ( (isinstance(value, type) and issubclass(value, upath.UPath)) or isinstance(value, str) ): raise ValueError( f"expected UPath subclass or FQN-string, got: {type(value).__name__!r}" ) if not item or item in self._m: get_upath_class.cache_clear() # type: ignore[attr-defined] _get_implementation_protocols.cache_clear() # type: ignore[attr-defined] self._m[item] = value def __delitem__(self, __v: str) -> None: raise NotImplementedError("removal is unsupported") def __len__(self) -> int: return len(set().union(self._m, self._entries)) def __iter__(self) -> Iterator[str]: return iter(set().union(self._m, self._entries)) _registry = _Registry() def available_implementations(*, fallback: bool = False) -> list[str]: """return a list of protocols for available implementations Parameters ---------- fallback: If True, also return protocols for fsspec filesystems without an implementation in universal_pathlib. """ if not fallback: return list(_registry) else: return list({*_registry, *_fsspec_registry, *_fsspec_known_implementations}) def register_implementation( protocol: str, cls: type[upath.UPath] | str, *, clobber: bool = False, ) -> None: """register a UPath implementation with a protocol Parameters ---------- protocol: Protocol name to associate with the class cls: The UPath subclass for the protocol or a str representing the full path to an implementation class like package.module.class. clobber: Whether to overwrite a protocol with the same name; if False, will raise instead. """ if not re.match(r"^[a-z][a-z0-9+_.]+$", protocol): raise ValueError(f"{protocol!r} is not a valid URI scheme") if not clobber and protocol in _registry: raise ValueError(f"{protocol!r} is already in registry and clobber is False!") _registry[protocol] = cls @lru_cache # type: ignore[misc] def _get_implementation_protocols(cls: type[upath.UPath]) -> list[str]: """return protocols registered for a given UPath class without triggering imports""" if not issubclass(cls, upath.UPath): raise ValueError(f"{cls!r} is not a UPath subclass") if cls.__module__ == "upath.implementations._experimental": # experimental fallback implementations have no registry entry return [cls.__name__[1:-4].lower()] loaded = ( p for p, c in _registry._m.maps[0].items() # type: ignore[attr-defined] if c is cls ) known = ( p for p, fqn in _registry.known_implementations.items() if fqn == f"{cls.__module__}.{cls.__name__}" ) eps = ( p for p, ep in _registry._entries.items() if ep.module == cls.__module__ and ep.attr == cls.__name__ ) return list(dict.fromkeys((*loaded, *known, *eps))) # --- get_upath_class type overloads ------------------------------------------ if TYPE_CHECKING: # noqa: C901 @overload def get_upath_class(protocol: Literal["simplecache"]) -> type[_SimpleCachePath]: ... @overload def get_upath_class(protocol: Literal["s3", "s3a"]) -> type[_S3Path]: ... @overload def get_upath_class(protocol: Literal["gcs", "gs"]) -> type[_GCSPath]: ... @overload # noqa: E301 def get_upath_class( protocol: Literal["abfs", "abfss", "adl", "az"], ) -> type[_AzurePath]: ... @overload def get_upath_class(protocol: Literal["data"]) -> type[_DataPath]: ... @overload def get_upath_class(protocol: Literal["ftp"]) -> type[_FTPPath]: ... @overload def get_upath_class(protocol: Literal["github"]) -> type[_GitHubPath]: ... @overload def get_upath_class(protocol: Literal["hdfs"]) -> type[_HDFSPath]: ... @overload def get_upath_class(protocol: Literal["hf"]) -> type[_HfPath]: ... @overload def get_upath_class(protocol: Literal["http", "https"]) -> type[_HTTPPath]: ... @overload def get_upath_class(protocol: Literal["file", "local"]) -> type[_FilePath]: ... @overload def get_upath_class(protocol: Literal["memory"]) -> type[_MemoryPath]: ... @overload def get_upath_class(protocol: Literal["sftp", "ssh"]) -> type[_SFTPPath]: ... @overload def get_upath_class(protocol: Literal["smb"]) -> type[_SMBPath]: ... @overload def get_upath_class(protocol: Literal["tar"]) -> type[_TarPath]: ... @overload def get_upath_class(protocol: Literal["webdav"]) -> type[_WebdavPath]: ... @overload def get_upath_class(protocol: Literal["zip"]) -> type[_ZipPath]: ... if sys.platform == "win32": @overload def get_upath_class(protocol: Literal[""]) -> type[_WindowsUPath]: ... else: @overload def get_upath_class(protocol: Literal[""]) -> type[_PosixUPath]: ... # type: ignore[overload-overlap] # noqa: E501 @overload def get_upath_class( protocol: str, *, fallback: bool = ... ) -> type[upath.UPath] | None: ... @lru_cache # type: ignore[misc] # see: https://github.com/python/typeshed/issues/11280 def get_upath_class( protocol: str, *, fallback: bool = True, ) -> type[upath.UPath] | None: """Return the upath cls for the given protocol. Returns `None` if no matching protocol can be found. Parameters ---------- protocol: The protocol string fallback: If fallback is False, don't return UPath instances for fsspec filesystems that don't have an implementation registered. """ try: return _registry[protocol] except KeyError: if not protocol: if os.name == "nt": from upath.implementations.local import WindowsUPath return WindowsUPath # type: ignore[return-value] else: from upath.implementations.local import PosixUPath return PosixUPath # type: ignore[return-value] if not fallback: return None try: get_filesystem_class(protocol) except ValueError: return None # this is an unknown protocol else: warnings.warn( f"UPath {protocol!r} filesystem not explicitly implemented." " Falling back to default implementation." " This filesystem may not be tested.", UserWarning, stacklevel=2, ) import upath.implementations._experimental as upath_experimental cls_name = f"_{protocol.title()}Path" cls = type( cls_name, (upath.UPath,), {"__module__": "upath.implementations._experimental"}, ) setattr(upath_experimental, cls_name, cls) return cls universal_pathlib-0.3.10/upath/tests/000077500000000000000000000000001514661127100175755ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/__init__.py000066400000000000000000000000001514661127100216740ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/cases.py000066400000000000000000001066411514661127100212550ustar00rootroot00000000000000import os import pickle import stat import sys import warnings from pathlib import Path import pytest from fsspec import __version__ as fsspec_version from fsspec import filesystem from fsspec import get_filesystem_class from packaging.version import Version from pathlib_abc import PathParser from pathlib_abc import vfspath from upath import UnsupportedOperation from upath import UPath from upath._protocol import get_upath_protocol from upath._stat import UPathStatResult from upath.tests.utils import posixify from upath.types import StatResultType class JoinablePathTests: """Tests for JoinablePath interface. These tests verify pure path operations that don't require filesystem access: - Path parsing and components (parts, parents, name, stem, suffix, etc.) - Path manipulation (with_name, with_suffix, with_stem, joinpath, etc.) - Path comparison and hashing - Serialization (pickling) - URI handling """ path: UPath def test_is_correct_class(self): raise NotImplementedError("must override") def test_parser(self): parser = self.path.parser assert isinstance(parser, PathParser) assert isinstance(parser.sep, str) assert parser.altsep is None or isinstance(parser.altsep, str) assert callable(parser.split) assert callable(parser.splitext) assert callable(parser.normcase) def test_with_segments(self): p = self.path.with_segments(self.path.__vfspath__(), "folder", "file.txt") assert p.parts[-2:] == ("folder", "file.txt") assert type(p) is type(self.path) def test___vfspath__(self): assert hasattr(self.path, "__vfspath__") assert callable(self.path.__vfspath__) str_path = vfspath(self.path) assert isinstance(str_path, str) def test_anchor(self): anchor = self.path.anchor assert isinstance(anchor, str) assert anchor == self.path.drive + self.path.root def test_is_absolute(self): assert self.path.is_absolute() is True def test_parents(self): p = self.path.joinpath("folder1", "file1.txt") assert p.parents[0] == p.parent assert p.parents[1] == p.parent.parent assert p.parents[0].name == "folder1" assert p.parents[1].name == self.path.name def test_with_name(self): path = self.path / "file.txt" path = path.with_name("file.zip") assert path.name == "file.zip" def test_with_suffix(self): path = self.path / "file.txt" path = path.with_suffix(".zip") assert path.suffix == ".zip" def test_suffix(self): path = self.path / "no_suffix" assert path.suffix == "" path = self.path / "file.txt" assert path.suffix == ".txt" path = self.path / "archive.tar.gz" assert path.suffix == ".gz" def test_suffixes(self): path = self.path / "no_suffix" assert path.suffixes == [] path = self.path / "file.txt" assert path.suffixes == [".txt"] path = self.path / "archive.tar.gz" assert path.suffixes == [".tar", ".gz"] def test_with_stem(self): if sys.version_info < (3, 9): pytest.skip("with_stem only available on py3.9+") path = self.path / "file.txt" path = path.with_stem("document") assert path.stem == "document" def test_repr_after_with_name(self): p = self.path.joinpath("file.txt").with_name("file.zip") assert "file.zip" in repr(p) def test_repr_after_with_suffix(self): p = self.path.joinpath("file.txt").with_suffix(".zip") assert "file.zip" in repr(p) def test_child_path(self): path_str = self.path.__vfspath__() path_a = UPath( path_str, "folder", protocol=self.path.protocol, **self.path.storage_options ) path_b = self.path / "folder" assert str(path_a) == str(path_b) assert path_a.root == path_b.root assert path_a.drive == path_b.drive def test_copy_path(self): path = self.path copy_path = UPath(path) assert type(path) is type(copy_path) assert str(path) == str(copy_path) assert path.drive == copy_path.drive assert path.root == copy_path.root assert path.parts == copy_path.parts assert path.storage_options == copy_path.storage_options def test_pickling(self): path = self.path pickled_path = pickle.dumps(path) recovered_path = pickle.loads(pickled_path) assert type(path) is type(recovered_path) assert str(path) == str(recovered_path) assert path.storage_options == recovered_path.storage_options def test_pickling_child_path(self): path = self.path / "subfolder" / "subsubfolder" pickled_path = pickle.dumps(path) recovered_path = pickle.loads(pickled_path) assert type(path) is type(recovered_path) assert str(path) == str(recovered_path) assert path.drive == recovered_path.drive assert path.root == recovered_path.root assert path.parts == recovered_path.parts assert path.storage_options == recovered_path.storage_options def test_as_uri(self): # test that we can reconstruct the path from the uri p0 = self.path uri = p0.as_uri() p1 = UPath(uri, **p0.storage_options) assert p0 == p1 def test_protocol(self): protocol = self.path.protocol fs_cls = get_filesystem_class(protocol) protocols = [p] if isinstance((p := fs_cls.protocol), str) else p print(protocol, protocols) assert protocol in protocols def test_hashable(self): assert hash(self.path) def test_storage_options_dont_affect_hash(self): cls = type(self.path) p0 = cls(str(self.path), test_extra=1, **self.path.storage_options) p1 = cls(str(self.path), test_extra=2, **self.path.storage_options) assert hash(p0) == hash(p1) def test_eq(self): cls = type(self.path) p0 = cls(str(self.path), test_extra=1, **self.path.storage_options) p1 = cls(str(self.path), test_extra=1, **self.path.storage_options) p2 = cls(str(self.path), test_extra=2, **self.path.storage_options) assert p0 == p1 assert p0 != p2 assert p1 != p2 def test_relative_to(self): base = self.path child = self.path / "folder1" / "file1.txt" relative = child.relative_to(base) assert str(relative) == "folder1/file1.txt" def test_is_relative_to(self): base = self.path child = self.path / "folder1" / "file1.txt" other = UPath("/some/other/path") assert child.is_relative_to(base) is True assert base.is_relative_to(child) is False assert child.is_relative_to(other) is False def test_full_match(self): p = self.path / "folder" / "file.txt" assert p.full_match("**/*") is True assert p.full_match("**/*.txt") is True assert p.full_match("*.doesnotexist") is False def test_trailing_slash_joinpath_is_identical(self): # setup cls = type(self.path) protocol = self.path.protocol path = self.path.path sopts = self.path.storage_options if not path: path = "something" path_with_slash = "something/" elif path.endswith("/"): path_with_slash = path path = path.removeprefix("/") else: path_with_slash = path + "/" key = "key/" # test a = cls(path_with_slash + key, protocol=protocol, **sopts) b = cls(path_with_slash, key, protocol=protocol, **sopts) c = cls(path_with_slash, protocol=protocol, **sopts).joinpath(key) d = cls(path_with_slash, protocol=protocol, **sopts) / key assert a.path == b.path == c.path == d.path def test_trailing_slash_is_stripped(self): has_meaningful_trailing_slash = getattr( self.path.parser, "has_meaningful_trailing_slash", False ) if has_meaningful_trailing_slash: assert not self.path.joinpath("key").path.endswith("/") assert self.path.joinpath("key/").path.endswith("/") else: assert not self.path.joinpath("key").path.endswith("/") assert not self.path.joinpath("key/").path.endswith("/") def test_parents_are_absolute(self): # this is a cross implementation compatible way to ensure that # the path representing the root is absolute is_absolute = [p.is_absolute() for p in self.path.parents] assert all(is_absolute) def test_parents_end_at_anchor(self): p = self.path.joinpath("folder1", "file1.txt") assert p.parents[-1].path == posixify(p.anchor) def test_anchor_is_its_own_parent(self): p = self.path.joinpath("folder1", "file1.txt") p0 = p.parents[-1] assert p0.path == posixify(p.anchor) assert p0.parent.path == posixify(p.anchor) def test_private_url_attr_in_sync(self): p = self.path p1 = self.path.joinpath("c") p2 = self.path / "c" assert p1._url == p2._url assert p1._url != p._url assert p1.protocol == p2.protocol class ReadablePathTests: """Tests for ReadablePath interface. These tests verify operations that read from the filesystem: - File/directory existence and type checks (exists, is_dir, is_file, etc.) - File metadata (stat, info) - Reading file contents (read_bytes, read_text, open for reading) - Directory listing (iterdir, glob, rglob) - Copy operations (read source) """ path: UPath @pytest.fixture(autouse=True) def path_file(self, path): self.path_file = self.path.joinpath("file1.txt") def test_storage_options_match_fsspec(self): storage_options = self.path.storage_options assert storage_options == self.path.fs.storage_options def test_stat(self): stat_ = self.path.stat() # for debugging os.stat_result compatibility attrs = {attr for attr in dir(stat_) if attr.startswith("st_")} print(attrs) assert isinstance(stat_, StatResultType) assert len(tuple(stat_)) == os.stat_result.n_sequence_fields with warnings.catch_warnings(): warnings.simplefilter("error") for idx in range(os.stat_result.n_sequence_fields): assert isinstance(stat_[idx], int) for attr in UPathStatResult._fields + UPathStatResult._fields_extra: assert hasattr(stat_, attr) def test_stat_dir_st_mode(self): base = self.path.stat() # base folder assert stat.S_ISDIR(base.st_mode) def test_stat_file_st_mode(self): file1 = self.path_file.stat() assert stat.S_ISREG(file1.st_mode) def test_stat_st_size(self): file1 = self.path_file.stat() assert file1.st_size == 11 @pytest.mark.parametrize( "url, expected", [("file1.txt", True), ("fakefile.txt", False)] ) def test_exists(self, url, expected): path = self.path.joinpath(url) assert path.exists() == expected def test_expanduser(self): assert self.path.expanduser() == self.path @pytest.mark.parametrize( "pattern", ( "*.txt", "*", pytest.param( "**/*.txt", marks=( pytest.mark.xfail(reason="requires fsspec>=2023.9.0") if Version(fsspec_version) < Version("2023.9.0") else () ), ), ), ) def test_glob(self, pathlib_base, pattern): mock_glob = list(self.path.glob(pattern)) path_glob = list(pathlib_base.glob(pattern)) _mock_start = len(self.path.parts) mock_glob_normalized = sorted( [tuple(filter(None, a.parts[_mock_start:])) for a in mock_glob] ) _path_start = len(pathlib_base.parts) path_glob_normalized = sorted([a.parts[_path_start:] for a in path_glob]) print(mock_glob_normalized, path_glob_normalized) assert mock_glob_normalized == path_glob_normalized def test_is_dir(self): assert self.path.is_dir() path = self.path / "file1.txt" assert not path.is_dir() assert not (self.path / "not-existing-dir").is_dir() def test_is_file(self): path_exists = self.path / "file1.txt" assert path_exists.is_file() assert not (self.path / "not-existing-file.txt").is_file() def test_is_mount(self): try: self.path.is_mount() except UnsupportedOperation: pytest.skip(f"is_mount() not supported for {type(self.path).__name__}") else: assert self.path.is_mount() is False def test_is_symlink(self): assert self.path.is_symlink() is False def test_is_socket(self): assert self.path.is_socket() is False def test_is_fifo(self): assert self.path.is_fifo() is False def test_is_block_device(self): assert self.path.is_block_device() is False def test_is_char_device(self): assert self.path.is_char_device() is False def test_iterdir(self, local_testdir): pl_path = Path(local_testdir) up_iter = list(self.path.iterdir()) pl_iter = list(pl_path.iterdir()) for x in up_iter: assert x.name != "" assert x.exists() assert len(up_iter) == len(pl_iter) assert {p.name for p in pl_iter} == {u.name for u in up_iter} def test_iterdir_parent_iteration(self): assert next(self.path.parent.iterdir()).exists() def test_iterdir2(self, local_testdir): pl_path = Path(local_testdir) / "folder1" up_iter = list((self.path / "folder1").iterdir()) pl_iter = list(pl_path.iterdir()) for x in up_iter: assert x.exists() assert len(up_iter) == len(pl_iter) assert {p.name for p in pl_iter} == {u.name for u in up_iter} def test_iterdir_trailing_slash(self): files_noslash = list(self.path.joinpath("folder1").iterdir()) files_slash = list(self.path.joinpath("folder1/").iterdir()) assert files_noslash == files_slash def test_lstat(self): with pytest.warns(UserWarning, match=r"[A-Za-z]+.stat"): st = self.path.lstat() assert st is not None def test_cwd(self): with pytest.raises(UnsupportedOperation): self.path.cwd() def test_home(self): with pytest.raises(UnsupportedOperation): self.path.home() def test_open(self): p = self.path_file with p.open(mode="r") as f: assert f.read() == "hello world" with p.open(mode="rb") as f: assert f.read() == b"hello world" def test_open_buffering(self): p = self.path_file p.open(buffering=-1) def test_open_block_size(self): p = self.path_file with p.open(mode="r", block_size=8192) as f: assert f.read() == "hello world" def test_open_errors(self): p = self.path_file with p.open(mode="r", encoding="ascii", errors="strict") as f: assert f.read() == "hello world" def test_read_bytes(self): mock = self.path.joinpath("file2.txt") assert mock.read_bytes() == b"hello world" def test_read_text(self): upath = self.path.joinpath("file1.txt") assert upath.read_text() == "hello world" def test_read_text_encoding(self): upath = self.path_file content = upath.read_text(encoding="utf-8") assert content == "hello world" def test_read_text_errors(self): upath = self.path_file content = upath.read_text(encoding="ascii", errors="strict") assert content == "hello world" def test_rglob(self, pathlib_base): pattern = "*.txt" result = [*self.path.rglob(pattern)] expected = [*pathlib_base.rglob(pattern)] assert len(result) == len(expected) def test_walk(self, local_testdir): def _raise(x): raise x # collect walk results from UPath upath_walk = [] for dirpath, dirnames, filenames in self.path.walk(on_error=_raise): rel_dirpath = dirpath.relative_to(self.path) upath_walk.append((str(rel_dirpath), sorted(dirnames), sorted(filenames))) upath_walk.sort() # collect walk results using os.walk (compatible with Python 3.9+) os_walk = [] for dirpath, dirnames, filenames in os.walk(local_testdir): rel_dirpath = os.path.relpath(dirpath, local_testdir) os_walk.append((rel_dirpath, sorted(dirnames), sorted(filenames))) os_walk.sort() assert upath_walk == os_walk def test_walk_top_down_false(self): def _raise(x): raise x # test walk with top_down=False returns directories after their contents paths_seen = [] for dirpath, _, _ in self.path.walk(top_down=False, on_error=_raise): paths_seen.append(dirpath) # in bottom-up walk, parent directories should come after children for i, path in enumerate(paths_seen): for _, other in enumerate(paths_seen[i + 1 :], start=i + 1): # if path is a parent of other, path should come after other if other.is_relative_to(path) and other != path: pytest.fail(f"In bottom-up walk, {path} should come after {other}") def test_samefile(self): f1 = self.path.joinpath("file1.txt") f2 = self.path.joinpath("file2.txt") assert f1.samefile(f2) is False assert f1.samefile(f2.path) is False assert f1.samefile(f1) is True assert f1.samefile(f1.path) is True def test_info(self): p0 = self.path.joinpath("file1.txt") p1 = self.path.joinpath("folder1") assert p0.info.exists() is True assert p0.info.is_file() is True assert p0.info.is_dir() is False assert p0.info.is_symlink() is False assert p1.info.exists() is True assert p1.info.is_file() is False assert p1.info.is_dir() is True assert p1.info.is_symlink() is False def test_copy_local(self, tmp_path: Path): target = UPath(tmp_path) / "target-file1.txt" source = self.path_file content = source.read_text() source.copy(target) assert target.exists() assert target.read_text() == content @pytest.mark.parametrize("target_type", [str, Path, UPath]) def test_copy_into__file_to_str_tempdir(self, tmp_path: Path, target_type): tmp_path = tmp_path.joinpath("somewhere") tmp_path.mkdir() target_dir = target_type(tmp_path) assert isinstance(target_dir, target_type) source = self.path_file source.copy_into(target_dir) target = tmp_path.joinpath(source.name) assert target.exists() assert target.read_text() == source.read_text() @pytest.mark.parametrize("target_type", [str, Path, UPath]) def test_copy_into__dir_to_str_tempdir(self, tmp_path: Path, target_type): tmp_path = tmp_path.joinpath("somewhere") tmp_path.mkdir() target_dir = target_type(tmp_path) assert isinstance(target_dir, target_type) source_dir = self.path.joinpath("folder1") assert source_dir.is_dir() source_dir.copy_into(target_dir) target = tmp_path.joinpath(source_dir.name) assert target.exists() assert target.is_dir() for item in source_dir.iterdir(): target_item = target.joinpath(item.name) assert target_item.exists() if item.is_file(): assert target_item.read_text() == item.read_text() def test_copy_into_local(self, tmp_path: Path): target_dir = UPath(tmp_path) / "target-dir" target_dir.mkdir() source = self.path_file content = source.read_text() source.copy_into(target_dir) target = target_dir / source.name assert target.exists() assert target.read_text() == content def test_copy_memory(self, clear_fsspec_memory_cache): target = UPath("memory:///target-file1.txt") source = self.path_file content = source.read_text() source.copy(target) assert target.exists() assert target.read_text() == content def test_copy_into_memory(self, clear_fsspec_memory_cache): target_dir = UPath("memory:///target-dir") target_dir.mkdir() source = self.path_file content = source.read_text() source.copy_into(target_dir) target = target_dir / source.name assert target.exists() assert target.read_text() == content def test_copy_exceptions(self, tmp_path: Path): source = self.path_file # target is a directory target = UPath(tmp_path) / "target-folder" target.mkdir() # FIXME: pytest.raises(IsADirectoryError) not working on Windows with pytest.raises(OSError): source.copy(target) # target parent does not exist target = UPath(tmp_path) / "nonexistent-dir" / "target-file1.txt" with pytest.raises(FileNotFoundError): source.copy(target) def test_copy_into_exceptions(self, tmp_path: Path): source = self.path_file # target is not a directory target_file = UPath(tmp_path) / "target-file.txt" target_file.write_text("content") # FIXME: pytest.raises(NotADirectoryError) not working on Windows with pytest.raises(OSError): source.copy_into(target_file) # target dir does not exist target_dir = UPath(tmp_path) / "nonexistent-dir" with pytest.raises(FileNotFoundError): source.copy_into(target_dir) def test_read_with_fsspec(self): p = self.path_file protocol = p.protocol storage_options = p.storage_options path = p.path fs = filesystem(protocol, **storage_options) with fs.open(path) as f: assert f.read() == b"hello world" def test_readlink(self): with pytest.raises(UnsupportedOperation): self.path.readlink() def test_group(self): with pytest.raises(UnsupportedOperation): self.path.group() def test_owner(self): with pytest.raises(UnsupportedOperation): self.path.owner() # ============================================================================= # WritablePathTests: Tests for writable path operations # ============================================================================= class _CommonWritablePathTests: SUPPORTS_EMPTY_DIRS = True path: UPath def test_chmod(self): with pytest.raises(NotImplementedError): self.path_file.chmod(777) def test_lchmod(self): with pytest.raises(UnsupportedOperation): self.path.lchmod(mode=0o777) def test_symlink_to(self): with pytest.raises(UnsupportedOperation): self.path_file.symlink_to("target") with pytest.raises(UnsupportedOperation): self.path.joinpath("link").symlink_to("target") def test_hardlink_to(self): with pytest.raises(UnsupportedOperation): self.path_file.symlink_to("target") with pytest.raises(UnsupportedOperation): self.path.joinpath("link").hardlink_to("target") class NonWritablePathTests(_CommonWritablePathTests): def test_mkdir_raises(self): with pytest.raises(UnsupportedOperation): self.path.mkdir() def test_touch_raises(self): with pytest.raises(UnsupportedOperation): self.path.touch() def test_unlink(self): with pytest.raises(UnsupportedOperation): self.path.unlink() def test_write_bytes(self): with pytest.raises(UnsupportedOperation): self.path_file.write_bytes(b"abc") def test_write_text(self): with pytest.raises(UnsupportedOperation): self.path_file.write_text("abc") class WritablePathTests(_CommonWritablePathTests): """Tests for WritablePath interface. These tests verify operations that write to the filesystem: - Creating directories (mkdir) - Creating files (touch) - Writing file contents (write_bytes, write_text) - Removing files/directories (unlink, rmdir) """ def test_mkdir(self): new_dir = self.path.joinpath("new_dir") new_dir.mkdir() if not self.SUPPORTS_EMPTY_DIRS: new_dir.joinpath(".file").touch() assert new_dir.exists() def test_mkdir_exists_ok_true(self): new_dir = self.path.joinpath("new_dir_may_exists") new_dir.mkdir() if not self.SUPPORTS_EMPTY_DIRS: new_dir.joinpath(".file").touch() new_dir.mkdir(exist_ok=True) def test_mkdir_exists_ok_false(self): new_dir = self.path.joinpath("new_dir_may_not_exists") new_dir.mkdir() if not self.SUPPORTS_EMPTY_DIRS: new_dir.joinpath(".file").touch() with pytest.raises(FileExistsError): new_dir.mkdir(exist_ok=False) def test_mkdir_parents_true_exists_ok_true(self): new_dir = self.path.joinpath("parent", "new_dir_may_not_exist") new_dir.mkdir(parents=True) if not self.SUPPORTS_EMPTY_DIRS: new_dir.joinpath(".file").touch() new_dir.mkdir(parents=True, exist_ok=True) def test_mkdir_parents_true_exists_ok_false(self): new_dir = self.path.joinpath("parent", "new_dir_may_exist") new_dir.mkdir(parents=True) if not self.SUPPORTS_EMPTY_DIRS: new_dir.joinpath(".file").touch() with pytest.raises(FileExistsError): new_dir.mkdir(parents=True, exist_ok=False) def test_touch_exists_ok_false(self): f = self.path.joinpath("file1.txt") assert f.exists() with pytest.raises(FileExistsError): f.touch(exist_ok=False) def test_touch_exists_ok_true(self): f = self.path.joinpath("file1.txt") assert f.exists() data = f.read_text() f.touch(exist_ok=True) assert f.read_text() == data def test_touch(self): path = self.path.joinpath("test_touch.txt") assert not path.exists() path.touch() assert path.exists() def test_touch_unlink(self): path = self.path.joinpath("test_touch.txt") path.touch() assert path.exists() path.unlink() assert not path.exists() # should raise FileNotFoundError since file is missing with pytest.raises(FileNotFoundError): path.unlink() # file doesn't exists, but missing_ok is True path.unlink(missing_ok=True) def test_write_bytes(self, pathlib_base): fn = "test_write_bytes.txt" s = b"hello_world" path = self.path.joinpath(fn) path.write_bytes(s) assert path.read_bytes() == s def test_write_text(self, pathlib_base): fn = "test_write_text.txt" s = "hello_world" path = self.path.joinpath(fn) path.write_text(s) assert path.read_text() == s def test_write_text_encoding(self): fn = "test_write_text_enc.txt" s = "hello_world" path = self.path.joinpath(fn) path.write_text(s, encoding="utf-8") assert path.read_text(encoding="utf-8") == s def test_write_text_errors(self): fn = "test_write_text_errors.txt" s = "hello_world" path = self.path.joinpath(fn) path.write_text(s, encoding="ascii", errors="strict") assert path.read_text(encoding="ascii") == s class ReadWritePathTests: """Tests requiring both ReadablePath and WritablePath interfaces. These tests verify operations that need both read and write access: - Rename/move operations - File system setup/teardown - Operations that verify write results by reading - rmdir operations """ SUPPORTS_EMPTY_DIRS = True path: UPath def test_rename(self): p_source = self.path.joinpath("file1.txt") p_target = self.path.joinpath("file1_renamed.txt") p_moved = p_source.rename(p_target) assert p_target == p_moved assert not p_source.exists() assert p_moved.exists() p_revert = p_moved.rename(p_source) assert p_revert == p_source assert not p_moved.exists() assert p_revert.exists() @pytest.fixture def supports_cwd(self): # intentionally called on the instance to support ProxyUPath().cwd() try: self.path.cwd() except UnsupportedOperation: return False else: return True @pytest.mark.parametrize( "target_factory", [ lambda obj, name: name, lambda obj, name: UPath(name), lambda obj, name: Path(name), lambda obj, name: obj.joinpath(name).relative_to(obj), ], ids=[ "str_relative", "plain_upath_relative", "plain_path_relative", "self_upath_relative", ], ) def test_rename_with_target_relative( self, request, monkeypatch, supports_cwd, target_factory, tmp_path ): source = self.path.joinpath("folder1/file2.txt") target = target_factory(self.path, "file2_renamed.txt") source_text = source.read_text() if supports_cwd: cid = request.node.callspec.id cwd = tmp_path.joinpath(cid) cwd.mkdir(parents=True, exist_ok=True) monkeypatch.chdir(cwd) t = source.rename(target) assert (t.protocol == UPath(target).protocol) or UPath( target ).protocol == "" assert (t.path == UPath(target).path) or ( t.path == UPath(target).absolute().path ) assert t.exists() assert t.read_text() == source_text else: with pytest.raises(UnsupportedOperation): source.rename(target) @pytest.mark.parametrize( "target_factory", [ lambda obj, name: obj.joinpath(name).absolute().as_posix(), lambda obj, name: UPath(obj.absolute().joinpath(name).path), lambda obj, name: Path(obj.absolute().joinpath(name).path), lambda obj, name: obj.absolute().joinpath(name), ], ids=[ "str_absolute", "plain_upath_absolute", "plain_path_absolute", "self_upath_absolute", ], ) def test_rename_with_target_absolute(self, target_factory): from upath._chain import Chain from upath._chain import FSSpecChainParser source = self.path.joinpath("folder1/file2.txt") target = target_factory(self.path, "file2_renamed.txt") source_text = source.read_text() t = source.rename(target) assert get_upath_protocol(target) in {t.protocol, ""} assert t.path == Chain.from_list( FSSpecChainParser().unchain(str(target)) ).active_path.replace("\\", "/") assert t.exists() assert t.read_text() == source_text def test_replace(self): pass def test_resolve(self): pass def test_rmdir_no_dir(self): p = self.path.joinpath("file1.txt") with pytest.raises(NotADirectoryError): p.rmdir() def test_iterdir_no_dir(self): p = self.path.joinpath("file1.txt") assert p.is_file() with pytest.raises(NotADirectoryError): _ = list(p.iterdir()) def test_rmdir_not_empty(self): p = self.path.joinpath("folder1") with pytest.raises(OSError, match="not empty"): p.rmdir(recursive=False) def test_fsspec_compat(self): fs = self.path.fs content = b"a,b,c\n1,2,3\n4,5,6" upath1 = self.path / "output1.csv" p1 = upath1.path upath1.write_bytes(content) assert fs._fs_token == upath1.fs._fs_token if fs.cachable: # codespell:ignore cachable assert fs is upath1.fs with fs.open(p1) as f: assert f.read() == content upath1.unlink() # write with fsspec, read with upath upath2 = self.path / "output2.csv" p2 = upath2.path if fs.cachable: # codespell:ignore cachable assert fs is upath2.fs with fs.open(p2, "wb") as f: f.write(content) assert upath2.read_bytes() == content upath2.unlink() def test_move_local(self, tmp_path: Path): target = UPath(tmp_path) / "target-file1.txt" source = self.path / "file1.txt" content = source.read_text() source.move(target) assert target.exists() assert target.read_text() == content assert not source.exists() def test_move_into_local(self, tmp_path: Path): target_dir = UPath(tmp_path) / "target-dir" target_dir.mkdir() source = self.path / "file1.txt" content = source.read_text() source.move_into(target_dir) target = target_dir / "file1.txt" assert target.exists() assert target.read_text() == content assert not source.exists() def test_move_memory(self, clear_fsspec_memory_cache): target = UPath("memory:///target-file1.txt") source = self.path / "file1.txt" content = source.read_text() source.move(target) assert target.exists() assert target.read_text() == content assert not source.exists() def test_move_into_memory(self, clear_fsspec_memory_cache): target_dir = UPath("memory:///target-dir") target_dir.mkdir() source = self.path / "file1.txt" content = source.read_text() source.move_into(target_dir) target = target_dir / "file1.txt" assert target.exists() assert target.read_text() == content assert not source.exists() def prepare_file_system(self): self.make_top_folder() self.make_test_files() def make_top_folder(self): self.path.mkdir(parents=True, exist_ok=True) def make_test_files(self): folder1 = self.path.joinpath("folder1") folder1.mkdir(exist_ok=True) folder1_files = ["file1.txt", "file2.txt"] for f in folder1_files: p = folder1.joinpath(f) p.touch() p.write_text(f) file1 = self.path.joinpath("file1.txt") file1.touch() file1.write_text("hello world") file2 = self.path.joinpath("file2.txt") file2.touch() file2.write_bytes(b"hello world") class BaseTests( JoinablePathTests, ReadablePathTests, WritablePathTests, ReadWritePathTests, ): """Comprehensive test suite combining all path operation tests. This class composes all the individual test suites for testing UPath implementations that support full read/write functionality. For UPath subclasses with limited functionality (e.g., read-only), use the appropriate subset of test classes: - JoinablePathTests: Pure path operations (no I/O) - ReadablePathTests: Read-only operations - WritablePathTests: Write-only operations - ReadWritePathTests: Operations requiring both read and write Example usage for a read-only UPath: class TestMyReadOnlyPath(JoinablePathTests, ReadablePathTests): @pytest.fixture(autouse=True) def setup(self, ...): self.path = MyReadOnlyUPath(...) """ SUPPORTS_EMPTY_DIRS = True path: UPath universal_pathlib-0.3.10/upath/tests/conftest.py000066400000000000000000000550311514661127100220000ustar00rootroot00000000000000import os import shlex import shutil import subprocess import sys import threading import time import uuid from pathlib import Path import fsspec import pytest from fsspec import get_filesystem_class from fsspec.implementations.local import LocalFileSystem from fsspec.implementations.local import make_path_posix from fsspec.implementations.smb import SMBFileSystem from fsspec.registry import _registry from fsspec.registry import register_implementation from fsspec.utils import stringify_path from packaging.version import Version from .utils import posixify class DummyTestFS(LocalFileSystem): protocol = "mock" root_marker = "/" @classmethod def _strip_protocol(cls, path): path = stringify_path(path) if path.startswith("mock://"): path = path[7:] elif path.startswith("mock:"): path = path[5:] return make_path_posix(path).rstrip("/") or cls.root_marker def unstrip_protocol(self, path): return f"mock://{self._strip_protocol(path)}" @pytest.fixture(scope="session") def clear_registry(): register_implementation("mock", DummyTestFS) try: yield finally: _registry.clear() @pytest.fixture(scope="function") def windows_working_directory_drive_sync(monkeypatch, tmp_path, tmp_path_factory): cwd_old = os.getcwd() drive_cwd = os.path.splitdrive(cwd_old)[0] drive_tmp = os.path.splitdrive(tmp_path)[0] if drive_tmp != drive_cwd: cwd_new = tmp_path_factory.mktemp("cwd_on_tmp_drive") os.chdir(cwd_new) try: yield finally: os.chdir(cwd_old) else: yield @pytest.fixture(scope="function") def clear_fsspec_memory_cache(): fs_cls = get_filesystem_class("memory") pseudo_dirs = fs_cls.pseudo_dirs.copy() store = fs_cls.store.copy() try: yield finally: fs_cls.pseudo_dirs = pseudo_dirs fs_cls.store = store @pytest.fixture(scope="function") def local_testdir(tmp_path, clear_registry): folder1 = tmp_path.joinpath("folder1") folder1.mkdir() folder1_files = ["file1.txt", "file2.txt"] for f in folder1_files: p = folder1.joinpath(f) p.touch() p.write_text(f) file1 = tmp_path.joinpath("file1.txt") file1.touch() file1.write_text("hello world") file2 = tmp_path.joinpath("file2.txt") file2.touch() file2.write_bytes(b"hello world") if sys.platform.startswith("win"): yield str(tmp_path).replace("\\", "/") else: yield str(tmp_path) @pytest.fixture() def pathlib_base(local_testdir): return Path(local_testdir) @pytest.fixture(scope="session") def htcluster(): try: proc = subprocess.Popen( shlex.split("htcluster startup"), stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) except FileNotFoundError as err: if err.errno == 2 and "htcluster" == err.filename: pytest.skip("htcluster not installed") raise time.sleep(30) try: yield finally: proc.terminate() proc.wait() proc1 = subprocess.Popen( shlex.split("htcluster shutdown"), stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) proc1.terminate() proc1.wait() time.sleep(10) @pytest.fixture() def hdfs(htcluster, tmp_path, local_testdir): pyarrow = pytest.importorskip("pyarrow") host, user, port = "0.0.0.0", "hdfs", 9000 hdfs = pyarrow.hdfs.connect(host="0.0.0.0", port=9000, user=user) hdfs.mkdir(str(tmp_path).encode("utf8"), create_parents=True) for x in Path(local_testdir).glob("**/*"): if x.is_file(): text = x.read_text().encode("utf8") if not hdfs.exists(str(x.parent)): hdfs.mkdir(str(x.parent), create_parents=True) with hdfs.open(str(x), "wb") as f: f.write(text) else: hdfs.mkdir(str(x)) hdfs.close() yield host, user, port @pytest.fixture(scope="session") def s3_server(): # writable local S3 system if "BOTO_CONFIG" not in os.environ: # pragma: no cover os.environ["BOTO_CONFIG"] = "/dev/null" if "AWS_ACCESS_KEY_ID" not in os.environ: # pragma: no cover os.environ["AWS_ACCESS_KEY_ID"] = "testing" if "AWS_SECRET_ACCESS_KEY" not in os.environ: # pragma: no cover os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" if "AWS_SECURITY_TOKEN" not in os.environ: # pragma: no cover os.environ["AWS_SECURITY_TOKEN"] = "testing" if "AWS_SESSION_TOKEN" not in os.environ: # pragma: no cover os.environ["AWS_SESSION_TOKEN"] = "testing" if "AWS_DEFAULT_REGION" not in os.environ: # pragma: no cover os.environ["AWS_DEFAULT_REGION"] = "us-east-1" requests = pytest.importorskip("requests") pytest.importorskip("moto") port = 5555 endpoint_uri = f"http://127.0.0.1:{port}/" proc = subprocess.Popen( [sys.executable, *shlex.split(f"-m moto.server -p {port}")], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, ) try: timeout = 5 while timeout > 0: try: r = requests.get(endpoint_uri, timeout=10) if r.ok: break except Exception: # pragma: no cover pass timeout -= 0.1 # pragma: no cover time.sleep(0.1) # pragma: no cover anon = False s3so = { "client_kwargs": {"endpoint_url": endpoint_uri}, "use_listings_cache": True, } yield anon, s3so finally: proc.terminate() proc.wait() @pytest.fixture def s3_fixture(s3_server, local_testdir): pytest.importorskip("s3fs") anon, s3so = s3_server s3 = fsspec.filesystem("s3", anon=False, **s3so) bucket_name = "test_bucket" if s3.exists(bucket_name): for dir, _, keys in s3.walk(bucket_name): for key in keys: s3.rm(f"{dir}/{key}") else: s3.mkdir(bucket_name) for x in Path(local_testdir).glob("**/*"): target_path = f"{bucket_name}/{posixify(x.relative_to(local_testdir))}" if x.is_file(): s3.upload(str(x), target_path) s3.invalidate_cache() yield f"s3://{bucket_name}", anon, s3so def stop_docker(container): cmd = shlex.split('docker ps -a -q --filter "name=%s"' % container) cid = subprocess.check_output(cmd).strip().decode() if cid: subprocess.call(["docker", "rm", "-f", "-v", cid]) @pytest.fixture(scope="session") def docker_gcs(): if "STORAGE_EMULATOR_HOST" in os.environ: # assume using real API or otherwise have a server already set up yield os.environ["STORAGE_EMULATOR_HOST"] return requests = pytest.importorskip("requests") if shutil.which("docker") is None: pytest.skip("docker not installed") container = "gcsfs_test" cmd = " ".join( [ "docker", "run", "-d", "-p 4443:4443", "--name gcsfs_test", "fsouza/fake-gcs-server:latest", "-scheme http", "-public-host http://localhost:4443", "-external-url http://localhost:4443", "-backend memory", ] ) stop_docker(container) subprocess.check_output(shlex.split(cmd)) url = "http://0.0.0.0:4443" timeout = 10 while True: try: r = requests.get(url + "/storage/v1/b", timeout=10) if r.ok: yield url break except Exception as e: # noqa: E722 timeout -= 1 if timeout < 0: raise SystemError from e time.sleep(1) stop_docker(container) @pytest.fixture def gcs_fixture(docker_gcs, local_testdir): pytest.importorskip("gcsfs") gcs = fsspec.filesystem("gcs", endpoint_url=docker_gcs, token="anon") bucket_name = "test_bucket" if gcs.exists(bucket_name): for dir, _, keys in gcs.walk(bucket_name): for key in keys: gcs.rm(f"{dir}/{key}") else: gcs.mkdir(bucket_name) for x in Path(local_testdir).glob("**/*"): target_path = f"{bucket_name}/{posixify(x.relative_to(local_testdir))}" if x.is_file(): gcs.upload(str(x), target_path) gcs.invalidate_cache() yield f"gs://{bucket_name}", docker_gcs @pytest.fixture(scope="session") def http_server(tmp_path_factory): http_tempdir = tmp_path_factory.mktemp("http") requests = pytest.importorskip("requests") pytest.importorskip("http.server") proc = subprocess.Popen( shlex.split(f"{sys.executable} -m http.server --directory {http_tempdir} 18080") ) try: url = "http://127.0.0.1:18080/folder" path = Path(http_tempdir) / "folder" path.mkdir() timeout = 10 while True: try: r = requests.get(url, timeout=10) if r.ok: yield path, url break except Exception as e: # noqa: E722 timeout -= 1 if timeout < 0: raise SystemError from e time.sleep(1) finally: proc.terminate() proc.wait() @pytest.fixture def http_fixture(local_testdir, http_server): http_path, http_url = http_server shutil.rmtree(http_path) shutil.copytree(local_testdir, http_path) yield http_url @pytest.fixture(scope="session") def webdav_server(tmp_path_factory): try: from cheroot import wsgi from wsgidav.wsgidav_app import WsgiDAVApp except ImportError as err: pytest.skip(str(err)) webdav_tmp_dir = str(tmp_path_factory.mktemp("webdav")) host = "127.0.0.1" port = 8090 app = WsgiDAVApp( { "host": host, "port": port, "provider_mapping": {"/": webdav_tmp_dir}, "simple_dc": {"user_mapping": {"*": {"USER": {"password": "PASSWORD"}}}}, } ) srvr = wsgi.Server(bind_addr=(host, port), wsgi_app=app) srvr.prepare() thread = threading.Thread(target=srvr.serve, daemon=True) thread.start() try: yield f"webdav+http://{host}:{port}", app finally: srvr.stop() @pytest.fixture def webdav_fixture(local_testdir, webdav_server): webdav_url, app = webdav_server # switch to new test directory fs_provider = app.provider_map["/"] fs_provider.root_folder_path = os.path.abspath(local_testdir) try: yield webdav_url finally: # clear locks if any are held fs_provider.lock_manager.storage.clear() AZURITE_PORT = int(os.environ.get("UPATH_AZURITE_PORT", "10000")) @pytest.fixture(scope="session") def azurite_credentials(): url = f"http://localhost:{AZURITE_PORT}" account_name = "devstoreaccount1" key = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" # noqa: E501 endpoint = f"{url}/{account_name}" connection_string = f"DefaultEndpointsProtocol=http;AccountName={account_name};AccountKey={key};BlobEndpoint={endpoint};" # noqa yield account_name, connection_string @pytest.fixture(scope="session") def docker_azurite(azurite_credentials): requests = pytest.importorskip("requests") if shutil.which("docker") is None: pytest.skip("docker not installed") image = "mcr.microsoft.com/azure-storage/azurite" container_name = "azure_test" cmd = ( f"docker run --rm -d -p {AZURITE_PORT}:10000 --name {container_name} {image}:latest" # noqa: E501 " azurite-blob --loose --blobHost 0.0.0.0 --skipApiVersionCheck" # noqa: E501 ) url = f"http://localhost:{AZURITE_PORT}" stop_docker(container_name) subprocess.run(shlex.split(cmd), check=True) retries = 10 while True: try: # wait until the container is up, even a 400 status code is ok r = requests.get(url, timeout=10) if ( r.status_code == 400 and "Server" in r.headers and "Azurite" in r.headers["Server"] ): yield url break except Exception as e: # noqa: E722 retries -= 1 if retries < 0: raise SystemError from e time.sleep(1) stop_docker(container_name) @pytest.fixture(scope="session") def azure_container(azurite_credentials, docker_azurite): azure_storage = pytest.importorskip("azure.storage.blob") account_name, connection_string = azurite_credentials client = azure_storage.BlobServiceClient.from_connection_string( conn_str=connection_string ) container_name = str(uuid.uuid4()) client.create_container(container_name) try: yield container_name finally: client.delete_container(container_name) @pytest.fixture(scope="function") def azure_fixture(azurite_credentials, azure_container): azure_storage = pytest.importorskip("azure.storage.blob") account_name, connection_string = azurite_credentials client = azure_storage.BlobServiceClient.from_connection_string( conn_str=connection_string ).get_container_client(azure_container) try: yield f"az://{azure_container}" finally: for blob in client.list_blobs(): client.delete_blob(blob["name"]) @pytest.fixture(scope="module") def smb_container(): try: pchk = ["docker", "run", "--name", "fsspec_test_smb", "hello-world"] subprocess.check_call(pchk) stop_docker("fsspec_test_smb") except (subprocess.CalledProcessError, FileNotFoundError): pytest.skip("docker run not available") # requires docker container = "fsspec_smb" stop_docker(container) cfg = "-p -u 'testuser;testpass' -s 'home;/share;no;no;no;testuser'" port = int(os.environ.get("UPATH_TESTS_SMB_PORT", "445")) img = f"docker run --name {container} --detach -p 139:139 -p {port}:445 dperson/samba" # noqa: E231 E501 cmd = f"{img} {cfg}" try: subprocess.check_output(shlex.split(cmd)).strip().decode() time.sleep(2) yield { "host": "localhost", "port": port, "username": "testuser", "password": "testpass", "register_session_retries": 100, # max ~= 10 seconds } finally: import smbclient # pylint: disable=import-outside-toplevel smbclient.reset_connection_cache() stop_docker(container) @pytest.fixture def smb_url(smb_container): smb_url = "smb://{username}:{password}@{host}:{port}/home/" smb_url = smb_url.format(**smb_container) return smb_url @pytest.fixture def smb_fixture(local_testdir, smb_url, smb_container): smb = SMBFileSystem( host=smb_container["host"], port=smb_container["port"], username=smb_container["username"], password=smb_container["password"], ) url = smb_url + "testdir/" smb.put(local_testdir, "/home/testdir", recursive=True) yield url smb.delete("/home/testdir", recursive=True) @pytest.fixture(scope="module") def ssh_container(): if shutil.which("docker") is None: pytest.skip("docker not installed") name = "fsspec_test_ssh" stop_docker(name) cmd = ( "docker run" " -d" f" --name {name}" " -e USER_NAME=user" " -e PASSWORD_ACCESS=true" " -e USER_PASSWORD=pass" " -p 2222:2222" " linuxserver/openssh-server:latest" ) try: subprocess.run(shlex.split(cmd)) yield { "host": "localhost", "port": 2222, "username": "user", "password": "pass", } finally: stop_docker(name) @pytest.fixture def ssh_fixture(ssh_container, local_testdir, monkeypatch): paramiko = pytest.importorskip("paramiko", reason="sftp tests require paramiko") cls = fsspec.get_filesystem_class("ssh") if cls.put != fsspec.AbstractFileSystem.put: monkeypatch.setattr(cls, "put", fsspec.AbstractFileSystem.put) if Version(fsspec.__version__) < Version("2022.10.0"): from fsspec.callbacks import _DEFAULT_CALLBACK monkeypatch.setattr(_DEFAULT_CALLBACK, "relative_update", lambda *args: None) for _ in range(100): try: fs = fsspec.filesystem( "ssh", host=ssh_container["host"], port=ssh_container["port"], username=ssh_container["username"], password=ssh_container["password"], timeout=10.0, banner_timeout=30.0, skip_instance_cache=True, ) except ( paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException, ): time.sleep(0.1) continue break else: raise RuntimeError("issue with openssh-container startup") fs.put(local_testdir, "/app/testdir", recursive=True) try: yield "ssh://{username}:{password}@{host}:{port}/app/testdir/".format( **ssh_container ) finally: fs.delete("/app/testdir", recursive=True) @pytest.fixture def hf_test_repo(): # "__username__" is an invalid username so we can use it for tests return "__username__/test_repo" @pytest.fixture def mock_hf_api(pathlib_base, monkeypatch, hf_test_repo): # noqa: C901 huggingface_hub = pytest.importorskip( "huggingface_hub", reason="hf tests require huggingface_hub" ) hf_file_system = pytest.importorskip( "huggingface_hub.hf_file_system", reason="hf tests require huggingface_hub" ) httpx = pytest.importorskip("httpx") class MockedHfApi(huggingface_hub.HfApi): def repo_info(self, repo_id, *args, repo_type=None, **kwargs): if repo_id != hf_test_repo: raise huggingface_hub.errors.RepositoryNotFoundError( repo_id, response=httpx.Response(404, request=...), ) elif repo_type is None or repo_type == "model": return huggingface_hub.hf_api.ModelInfo(id=repo_id) elif repo_type == "dataset": return huggingface_hub.hf_api.DatasetInfo(id=repo_id) elif repo_type == "space": return huggingface_hub.hf_api.SpaceInfo(id=repo_id) else: raise ValueError("Unsupported repo type.") def get_paths_info(self, repo_id, paths, *args, **kwargs): if repo_id != hf_test_repo: raise huggingface_hub.errors.RepositoryNotFoundError( repo_id, response=httpx.Response(404, request=...), ) paths_info = [] for path in paths: if path: path = pathlib_base / path if path.is_file(): paths_info.append( huggingface_hub.hf_api.RepoFile( path=path.relative_to(pathlib_base).as_posix(), blob_id="blob_id", size=path.stat().st_size, ) ) elif path.is_dir(): paths_info.append( huggingface_hub.hf_api.RepoFolder( path=path.relative_to(pathlib_base).as_posix(), tree_id="tree_id", ) ) return paths_info def list_repo_tree( self, repo_id, path_in_repo, *args, recursive=False, **kwargs ): if repo_id != hf_test_repo: raise huggingface_hub.errors.RepositoryNotFoundError( repo_id, response=httpx.Response(404, request=...) ) pathlib_dir = pathlib_base / path_in_repo if path_in_repo else pathlib_base for path in pathlib_dir.rglob("*") if recursive else pathlib_dir.glob("*"): if path.is_file(): yield huggingface_hub.hf_api.RepoFile( path=path.relative_to(pathlib_base).as_posix(), oid="oid", size=path.stat().st_size, ) else: yield huggingface_hub.hf_api.RepoFolder( path=path.relative_to(pathlib_base).as_posix(), oid="oid", ) hf_file_system.HfFileSystem.clear_instance_cache() monkeypatch.setattr(hf_file_system, "HfApi", MockedHfApi) @pytest.fixture def mock_hf_filesystem_open(pathlib_base, monkeypatch): hf_file_system = pytest.importorskip( "huggingface_hub.hf_file_system", reason="hf tests require huggingface_hub" ) def mocked_open(fs, path, mode="rb", *args, **kwargs): resolved_path = fs.resolve_path(path) return (pathlib_base / resolved_path.path_in_repo).open(mode) monkeypatch.setattr(hf_file_system.HfFileSystem, "_open", mocked_open) @pytest.fixture def hf_fixture_with_readonly_mocked_hf_api( hf_test_repo, mock_hf_api, mock_hf_filesystem_open ): return "hf://" + hf_test_repo @pytest.fixture(scope="module") def ftp_server_process(tmp_path_factory): """Fixture providing a writable FTP filesystem.""" pytest.importorskip("pyftpdlib") tmp_path = tmp_path_factory.mktemp("ftp-server") P = subprocess.Popen( [ sys.executable, "-m", "pyftpdlib", "-d", str(tmp_path), "-u", "user", "-P", "pass", "-w", ] ) try: time.sleep(1) yield str(tmp_path), { "host": "localhost", "port": 2121, "username": "user", "password": "pass", } finally: P.terminate() P.wait() try: shutil.rmtree(tmp_path) except Exception: pass @pytest.fixture(scope="function") def ftp_server(ftp_server_process): """Fixture providing a writable FTP filesystem.""" tmp_path, storage_options = ftp_server_process try: yield storage_options finally: for filename in os.listdir(tmp_path): file_path = os.path.join(tmp_path, filename) if os.path.isdir(file_path): del_func = shutil.rmtree else: del_func = os.unlink try: del_func(file_path) except Exception: pass universal_pathlib-0.3.10/upath/tests/implementations/000077500000000000000000000000001514661127100230055ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/implementations/__init__.py000066400000000000000000000000001514661127100251040ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/implementations/test_azure.py000066400000000000000000000072611514661127100255520ustar00rootroot00000000000000import warnings import fsspec import pytest from upath import UPath from upath.implementations.cloud import AzurePath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base from ..utils import posixify from ..utils import skip_on_windows @skip_on_windows class TestAzurePath(BaseTests, metaclass=OverrideMeta): SUPPORTS_EMPTY_DIRS = False @pytest.fixture(autouse=True, scope="function") def path(self, azurite_credentials, azure_fixture): account_name, connection_string = azurite_credentials self.storage_options = { "account_name": account_name, "connection_string": connection_string, } self.path = UPath(azure_fixture, **self.storage_options) self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, AzurePath) @overrides_base def test_protocol(self): # test all valid protocols for azure... protocol = self.path.protocol protocols = ["abfs", "abfss", "adl", "az"] assert protocol in protocols @extends_base def test_rmdir(self): new_dir = self.path / "new_dir_rmdir" new_dir.mkdir() path = new_dir / "test.txt" path.write_text("hello") assert path.exists() new_dir.rmdir() assert not new_dir.exists() with pytest.raises(NotADirectoryError): (self.path / "a" / "file.txt").rmdir() @extends_base def test_broken_mkdir(self): path = UPath( "az://new-container/", **self.storage_options, ) if path.exists(): path.rmdir() path.mkdir(parents=True, exist_ok=False) (path / "file").write_text("foo") assert path.exists() @skip_on_windows @pytest.mark.xfail(reason="adlfs returns isdir false") def test_copy__object_key_collides_with_dir_prefix(azurite_credentials, tmp_path): account_name, connection_string = azurite_credentials storage_options = { "account_name": account_name, "connection_string": connection_string, } az = fsspec.filesystem("az", **storage_options, use_listings_cache=False) container = "copy-into-collision-container" az.mkdir(container) # store more objects with same prefix az.pipe_file(f"{container}/src/common_prefix/file1.txt", b"1") az.pipe_file(f"{container}/src/common_prefix/file2.txt", b"2") # object under common prefix as key az.pipe_file(f"{container}/src/common_prefix", b"hello world") az.invalidate_cache() # make sure the sources have a collision assert az.isfile(f"{container}/src/common_prefix") assert az.isdir(f"{container}/src/common_prefix") assert az.isfile(f"{container}/src/common_prefix/file1.txt") assert az.isfile(f"{container}/src/common_prefix/file2.txt") # prepare source and destination src = UPath(f"az://{container}/src", **storage_options) dst = UPath(tmp_path) def on_collision_rename_file(src, dst): warnings.warn( f"{src!s} collides with prefix. Renaming target file object to {dst!s}", UserWarning, stacklevel=3, ) return ( dst.with_suffix(dst.suffix + ".COLLISION"), dst, ) # perform copy src.copy_into(dst, on_name_collision=on_collision_rename_file) # check results dst_files = sorted(posixify(x.relative_to(tmp_path)) for x in dst.glob("**/*")) assert dst_files == [ "src", "src/common_prefix", "src/common_prefix.COLLISION", "src/common_prefix/file1.txt", "src/common_prefix/file2.txt", ] universal_pathlib-0.3.10/upath/tests/implementations/test_cached.py000066400000000000000000000012171514661127100256260ustar00rootroot00000000000000import pytest from upath import UPath from upath.implementations.cached import SimpleCachePath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base class TestSimpleCachePath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): if not local_testdir.startswith("/"): local_testdir = "/" + local_testdir path = f"simplecache::memory:{local_testdir}" self.path = UPath(path) self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, SimpleCachePath) universal_pathlib-0.3.10/upath/tests/implementations/test_data.py000066400000000000000000000175341514661127100253410ustar00rootroot00000000000000import stat import pytest from upath import UnsupportedOperation from upath import UPath from upath.implementations.data import DataPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import overrides_base class TestUPathDataPath( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): """ Unit-tests for the DataPath implementation of UPath. """ @pytest.fixture(autouse=True) def path(self): path = "data:text/plain;base64,aGVsbG8gd29ybGQ=" self.path = UPath(path) @pytest.fixture(autouse=True) def path_file(self, path): self.path_file = self.path @overrides_base def test_is_correct_class(self): assert isinstance(self.path, DataPath) @overrides_base def test_with_segments(self): # DataPath does not support joins, so in all usual cases it'll raise with pytest.raises(UnsupportedOperation): self.path.with_segments("data:text/plain;base64,", "aGVsbG8K") # but you can instantiate with a single full url self.path.with_segments("data:text/plain;base64,aGVsbG8K") @overrides_base def test_parents(self): # DataPath is always a absolute path with no parents assert self.path.parents == [] @overrides_base def test_with_name(self): # DataPath does not support name changes with pytest.raises(UnsupportedOperation): self.path.with_name("newname") @overrides_base def test_with_suffix(self): # DataPath does not support suffix changes with pytest.raises(UnsupportedOperation): self.path.with_suffix(".new") @overrides_base def test_with_stem(self): # DataPath does not support stem changes with pytest.raises(UnsupportedOperation): self.path.with_stem("newname") @overrides_base def test_suffix(self): # DataPath does not have suffixes assert self.path.suffix == "" @overrides_base def test_suffixes(self): # DataPath does not have suffixes assert self.path.suffixes == [] @overrides_base def test_repr_after_with_name(self): with pytest.raises(UnsupportedOperation): repr(self.path.with_name("data:,ABC")) @overrides_base def test_repr_after_with_suffix(self): with pytest.raises(UnsupportedOperation): repr(self.path.with_suffix("")) @overrides_base def test_child_path(self): # DataPath does not support joins, so child paths are unsupported with pytest.raises(UnsupportedOperation): super().test_child_path() @overrides_base def test_pickling_child_path(self): # DataPath does not support joins, so child paths are unsupported with pytest.raises(UnsupportedOperation): super().test_pickling_child_path() @overrides_base def test_relative_to(self): # DataPath only relative_to with itself with pytest.raises(ValueError): self.path.relative_to("data:,ABC") self.path.relative_to(self.path) @overrides_base def test_is_relative_to(self): # DataPath only relative_to with itself assert not self.path.is_relative_to("data:,ABC") assert self.path.is_relative_to(self.path) @overrides_base def test_full_match(self): assert self.path.full_match("*") assert not self.path.full_match("xxx") @overrides_base def test_trailing_slash_joinpath_is_identical(self): # DataPath has no slashes, and is not joinable with pytest.raises(UnsupportedOperation): super().test_trailing_slash_joinpath_is_identical() @overrides_base def test_trailing_slash_is_stripped(self): # DataPath has no slashes, and is not joinable with pytest.raises(UnsupportedOperation): super().test_trailing_slash_is_stripped() @overrides_base def test_parents_end_at_anchor(self): # DataPath does not support joins with pytest.raises(UnsupportedOperation): super().test_parents_end_at_anchor() @overrides_base def test_anchor_is_its_own_parent(self): # DataPath does not support joins assert self.path.path == self.path.parent.path @overrides_base def test_private_url_attr_in_sync(self): # DataPath does not support joins, so we check on self.path assert self.path._url @overrides_base def test_stat_dir_st_mode(self): # DataPath does not have directories assert not stat.S_ISDIR(self.path.stat().st_mode) @overrides_base def test_exists(self): # A valid DataPath always exists assert self.path.exists() @overrides_base def test_glob(self): # DataPath does not have dirs, joins or globs assert list(self.path.glob("*")) == [] @overrides_base def test_rglob(self): # DataPath does not have dirs, joins or globs assert list(self.path.rglob("*")) == [] @overrides_base def test_is_dir(self): # DataPath does not have directories assert not self.path.is_dir() @overrides_base def test_is_file(self): # DataPath is always a file assert self.path.is_file() @overrides_base def test_iterdir(self): # DataPath does not have directories with pytest.raises(NotADirectoryError): self.path.iterdir() @overrides_base def test_iterdir_parent_iteration(self): with pytest.raises(NotADirectoryError): super().test_iterdir_parent_iteration() @overrides_base def test_iterdir2(self): # DataPath does not have directories, or joins with pytest.raises(NotADirectoryError): self.path_file.iterdir() @overrides_base def test_iterdir_trailing_slash(self): # DataPath does not have directories, or joins with pytest.raises(UnsupportedOperation): super().test_iterdir_trailing_slash() @overrides_base def test_read_bytes(self): assert self.path.read_bytes() == b"hello world" @overrides_base def test_read_text(self): assert self.path.read_text() == "hello world" @overrides_base def test_walk(self): # DataPath does not have directories assert list(self.path.walk()) == [] @overrides_base def test_walk_top_down_false(self): # DataPath does not have directories assert list(self.path.walk(top_down=False)) == [] @overrides_base def test_samefile(self): # DataPath doesn't have joins, so only identical paths are samefile f1 = UPath("data:text/plain;base64,aGVsbG8gd29ybGQ=") f2 = UPath("data:text/plain;base64,SGVsbG8gd29ybGQ=") assert f1.samefile(f2) is False assert f1.samefile(f2.path) is False assert f1.samefile(f1) is True assert f1.samefile(f1.path) is True @overrides_base def test_info(self): # DataPath info checks p0 = self.path assert p0.info.exists() is True assert p0.info.is_file() is True assert p0.info.is_dir() is False assert p0.info.is_symlink() is False @overrides_base def test_mkdir_raises(self): # DataPaths always exist and are files with pytest.raises(FileExistsError): self.path_file.mkdir() @overrides_base def test_touch_raises(self): # DataPaths always exist, so touch is a noop self.path_file.touch() @overrides_base def test_unlink(self): # DataPaths can't be deleted with pytest.raises(UnsupportedOperation): self.path_file.unlink() @overrides_base def test_copy_into__dir_to_str_tempdir(self): # There are no directories in DataPath assert not self.path.is_dir() universal_pathlib-0.3.10/upath/tests/implementations/test_ftp.py000066400000000000000000000015111514661127100252050ustar00rootroot00000000000000import pytest from upath import UPath from upath.implementations.ftp import FTPPath from upath.tests.cases import BaseTests from upath.tests.utils import skip_on_windows from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base @skip_on_windows class TestUPathFTP(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, ftp_server): self.path = UPath("", protocol="ftp", **ftp_server) self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, FTPPath) @extends_base def test_ftp_path_mtime(self, ftp_server): path = UPath("file1.txt", protocol="ftp", **ftp_server) path.touch() mtime = path.stat().st_mtime assert isinstance(mtime, float) universal_pathlib-0.3.10/upath/tests/implementations/test_gcs.py000066400000000000000000000064531514661127100252020ustar00rootroot00000000000000import warnings import fsspec import pytest from upath import UPath from upath.implementations.cloud import GCSPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base from ..utils import posixify from ..utils import skip_on_windows @skip_on_windows class TestGCSPath(BaseTests, metaclass=OverrideMeta): SUPPORTS_EMPTY_DIRS = False @pytest.fixture(autouse=True, scope="function") def path(self, gcs_fixture): path, endpoint_url = gcs_fixture self.path = UPath(path, endpoint_url=endpoint_url, token="anon") @overrides_base def test_is_correct_class(self): assert isinstance(self.path, GCSPath) @extends_base def test_rmdir(self): dirname = "rmdir_test" mock_dir = self.path.joinpath(dirname) mock_dir.joinpath("test.txt").write_text("hello") mock_dir.fs.invalidate_cache() mock_dir.rmdir() assert not mock_dir.exists() with pytest.raises(NotADirectoryError): self.path.joinpath("file1.txt").rmdir() @skip_on_windows def test_mkdir_in_empty_bucket(docker_gcs): fs = fsspec.filesystem("gcs", endpoint_url=docker_gcs, token="anon") fs.mkdir("my-fresh-bucket") assert "my-fresh-bucket/" in fs.buckets fs.invalidate_cache() del fs UPath( "gs://my-fresh-bucket/some-dir/another-dir/file", endpoint_url=docker_gcs, token="anon", ).parent.mkdir(parents=True, exist_ok=True) @skip_on_windows @pytest.mark.xfail(reason="gcsfs returns isdir false") def test_copy__object_key_collides_with_dir_prefix(docker_gcs, tmp_path): gcs = fsspec.filesystem( "gcs", endpoint_url=docker_gcs, token="anon", use_listings_cache=False, ) bucket = "copy_into_collision_bucket" gcs.mkdir(bucket) # gcs.mkdir(bucket + "/src" + "/common_prefix/") # object under common prefix as key gcs.pipe_file(f"{bucket}/src/common_prefix", b"hello world") # store more objects with same prefix gcs.pipe_file(f"{bucket}/src/common_prefix/file1.txt", b"1") gcs.pipe_file(f"{bucket}/src/common_prefix/file2.txt", b"2") gcs.invalidate_cache() # make sure the sources have a collision assert gcs.isfile(f"{bucket}/src/common_prefix") assert gcs.isdir(f"{bucket}/src/common_prefix") # BROKEN in gcsfs assert gcs.isfile(f"{bucket}/src/common_prefix/file1.txt") assert gcs.isfile(f"{bucket}/src/common_prefix/file2.txt") # prepare source and destination src = UPath(f"gs://{bucket}/src", endpoint_url=docker_gcs, token="anon") dst = UPath(tmp_path) def on_collision_rename_file(src, dst): warnings.warn( f"{src!s} collides with prefix. Renaming target file object to {dst!s}", UserWarning, stacklevel=3, ) return ( dst.with_suffix(dst.suffix + ".COLLISION"), dst, ) # perform copy src.copy_into(dst, on_name_collision=on_collision_rename_file) # check results dst_files = sorted(posixify(x.relative_to(tmp_path)) for x in dst.glob("**/*")) assert dst_files == [ "src", "src/common_prefix", "src/common_prefix.COLLISION", "src/common_prefix/file1.txt", "src/common_prefix/file2.txt", ] universal_pathlib-0.3.10/upath/tests/implementations/test_github.py000066400000000000000000000044701514661127100257050ustar00rootroot00000000000000import functools import os import platform import sys import pytest from upath import UPath from upath.implementations.github import GitHubPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import overrides_base pytestmark = pytest.mark.skipif( os.environ.get("CI", False) and not ( platform.system() == "Linux" and sys.version_info[:2] in {(3, 9), (3, 13)} ), reason="Skipping GitHubPath tests to prevent rate limiting on GitHub API.", ) def xfail_on_github_connection_error(func): """Method decorator to xfail tests on GitHub rate limit or connection errors.""" @functools.wraps(func) def wrapper(self, *args, **kwargs): try: return func(self, *args, **kwargs) except Exception as e: str_e = str(e) if "rate limit exceeded" in str_e or "too many requests for url" in str_e: pytest.xfail("GitHub API rate limit exceeded") elif ( "nodename nor servname provided, or not known" in str_e or "Network is unreachable" in str_e or "NameResolutionError" in str_e ): pytest.xfail("No internet connection") else: raise return wrapper def wrap_all_tests(decorator): """Class decorator factory to wrap all test methods with a given decorator.""" def class_decorator(cls): for attr_name in dir(cls): if attr_name.startswith("test_"): orig_method = getattr(cls, attr_name) setattr(cls, attr_name, decorator(orig_method)) return cls return class_decorator @wrap_all_tests(xfail_on_github_connection_error) class TestUPathGitHubPath( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): """ Unit-tests for the GitHubPath implementation of UPath. """ @pytest.fixture(autouse=True) def path(self): """ Fixture for the UPath instance to be tested. """ path = "github://ap--:universal_pathlib@test_data/data" self.path = UPath(path) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, GitHubPath) universal_pathlib-0.3.10/upath/tests/implementations/test_hdfs.py000066400000000000000000000012001514661127100253330ustar00rootroot00000000000000"""see upath/tests/conftest.py for fixtures""" import pytest # noqa: F401 from upath import UPath from upath.implementations.hdfs import HDFSPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base @pytest.mark.hdfs class TestUPathHDFS(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir, hdfs): host, user, port = hdfs path = f"hdfs:{local_testdir}" self.path = UPath(path, host=host, user=user, port=port) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, HDFSPath) universal_pathlib-0.3.10/upath/tests/implementations/test_hf.py000066400000000000000000000031701514661127100250140ustar00rootroot00000000000000import pytest from fsspec import get_filesystem_class from upath import UnsupportedOperation from upath import UPath from upath.implementations.cloud import HfPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import overrides_base try: get_filesystem_class("hf") except ImportError: pytestmark = pytest.mark.skip def test_hfpath(): path = UPath("hf://HuggingFaceTB/SmolLM2-135M") assert isinstance(path, HfPath) try: assert path.exists() except AssertionError: from httpx import ConnectError from huggingface_hub import HfApi try: HfApi().repo_info("HuggingFaceTB/SmolLM2-135M") except ConnectError: pytest.xfail("No internet connection") except Exception as err: if "Service Unavailable" in str(err): pytest.xfail("HuggingFace API not reachable") raise class TestUPathHf( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): @pytest.fixture(autouse=True, scope="function") def path(self, hf_fixture_with_readonly_mocked_hf_api): self.path = UPath(hf_fixture_with_readonly_mocked_hf_api) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, HfPath) @overrides_base def test_iterdir_parent_iteration(self): # HfPath does not support listing all available Repositories with pytest.raises(UnsupportedOperation): super().test_iterdir_parent_iteration() universal_pathlib-0.3.10/upath/tests/implementations/test_http.py000066400000000000000000000164051514661127100254030ustar00rootroot00000000000000import pytest # noqa: F401 from fsspec import get_filesystem_class from upath import UPath from upath.implementations.http import HTTPPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base from ..utils import skip_on_windows from ..utils import xfail_if_no_ssl_connection try: get_filesystem_class("http") except ImportError: pytestmark = pytest.mark.skip @pytest.fixture def internet_connection(): import requests try: requests.get("http://example.com") except requests.exceptions.ConnectionError: pytest.xfail(reason="No internet connection") else: yield def test_httppath(internet_connection): path = UPath("http://example.com") assert isinstance(path, HTTPPath) assert path.exists() @xfail_if_no_ssl_connection def test_httpspath(internet_connection): path = UPath("https://example.com") assert isinstance(path, HTTPPath) assert path.exists() @skip_on_windows class TestUPathHttp( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): @pytest.fixture(autouse=True, scope="function") def path(self, http_fixture): self.path = UPath(http_fixture) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, HTTPPath) @extends_base def test_work_at_root(self): assert "folder" in (f.name for f in self.path.parent.iterdir()) @extends_base def test_resolve(self): # Also tests following redirects, because the test server issues a # 301 redirect for `http://127.0.0.1:8080/folder` to # `http://127.0.0.1:8080/folder/` assert str(self.path.resolve()).endswith("/") @overrides_base def test_info(self): # HTTPPath folders are files too p0 = self.path.joinpath("file1.txt") p1 = self.path.joinpath("folder1") assert p0.info.exists() is True assert p0.info.is_file() is True assert p0.info.is_dir() is False assert p0.info.is_symlink() is False assert p1.info.exists() is True assert ( p1.info.is_file() is True ) # Weird quirk of how directories work in http fsspec assert p1.info.is_dir() is True assert p1.info.is_symlink() is False @pytest.mark.parametrize( "args,parts", [ (("http://example.com/"), ("http://example.com/", "")), (("http://example.com//"), ("http://example.com/", "", "")), (("http://example.com///"), ("http://example.com/", "", "", "")), (("http://example.com/a"), ("http://example.com/", "a")), (("http://example.com/a/"), ("http://example.com/", "a", "")), (("http://example.com/a/b"), ("http://example.com/", "a", "b")), (("http://example.com/a//b"), ("http://example.com/", "a", "", "b")), (("http://example.com/a//b/"), ("http://example.com/", "a", "", "b", "")), ], ) def test_empty_parts(args, parts): pth = UPath(args) pth_parts = pth.parts assert pth_parts == parts def test_query_parameters_passthrough(): pth = UPath("http://example.com/?a=1&b=2") assert pth.parts == ("http://example.com/", "?a=1&b=2") @pytest.mark.parametrize( "base,rel,expected", [ ( "http://www.example.com/a/b/index.html", "image.png?version=1", "http://www.example.com/a/b/image.png?version=1", ), ( "http://www.example.com/a/b/index.html", "../image.png", "http://www.example.com/a/image.png", ), ( "http://www.example.com/a/b/index.html", "/image.png", "http://www.example.com/image.png", ), ( "http://www.example.com/a/b/index.html", "sftp://other.com/image.png", "sftp://other.com/image.png", ), ( "http://www.example.com/a/b/index.html", "//other.com/image.png", "http://other.com/image.png", ), ], ) def test_joinuri_behavior(base, rel, expected): p0 = UPath(base) pr = p0.joinuri(rel) pe = UPath(expected) assert pr == pe NORMALIZATIONS = ( ("unnormalized", "normalized"), ( # Expected normalization results according to curl ("http://example.com", "http://example.com/"), ("http://example.com/", "http://example.com/"), ("http://example.com/a", "http://example.com/a"), ("http://example.com//a", "http://example.com//a"), ("http://example.com///a", "http://example.com///a"), ("http://example.com////a", "http://example.com////a"), ("http://example.com/a/.", "http://example.com/a/"), ("http://example.com/a/./", "http://example.com/a/"), ("http://example.com/a/./b", "http://example.com/a/b"), ("http://example.com/a/.//", "http://example.com/a//"), ("http://example.com/a/.//b", "http://example.com/a//b"), ("http://example.com/a//.", "http://example.com/a//"), ("http://example.com/a//./", "http://example.com/a//"), ("http://example.com/a//./b", "http://example.com/a//b"), ("http://example.com/a//.//", "http://example.com/a///"), ("http://example.com/a//.//b", "http://example.com/a///b"), ("http://example.com/a/..", "http://example.com/"), ("http://example.com/a/../", "http://example.com/"), ("http://example.com/a/../.", "http://example.com/"), ("http://example.com/a/../..", "http://example.com/"), ("http://example.com/a/../../", "http://example.com/"), ("http://example.com/a/../..//", "http://example.com//"), ("http://example.com/a/..//", "http://example.com//"), ("http://example.com/a/..//.", "http://example.com//"), ("http://example.com/a/..//..", "http://example.com/"), ("http://example.com/a/../b", "http://example.com/b"), ("http://example.com/a/..//b", "http://example.com//b"), ("http://example.com/a//..", "http://example.com/a/"), ("http://example.com/a//../", "http://example.com/a/"), ("http://example.com/a//../.", "http://example.com/a/"), ("http://example.com/a//../..", "http://example.com/"), ("http://example.com/a//../../", "http://example.com/"), ("http://example.com/a//../..//", "http://example.com//"), ("http://example.com/a//..//..", "http://example.com/a/"), ("http://example.com/a//../b", "http://example.com/a/b"), ("http://example.com/a//..//", "http://example.com/a//"), ("http://example.com/a//..//.", "http://example.com/a//"), ("http://example.com/a//..//b", "http://example.com/a//b"), ), ) @pytest.mark.parametrize(*NORMALIZATIONS) def test_normalize(unnormalized, normalized): expected = HTTPPath(normalized) pth = HTTPPath(unnormalized) assert expected.protocol in {"http", "https"} assert pth.protocol in {"http", "https"} # Normalise only, do not attempt to follow redirects for http:// paths here result = pth.resolve(strict=True, follow_redirects=False) str_expected = str(expected) str_result = str(result) assert expected == result assert str_expected == str_result universal_pathlib-0.3.10/upath/tests/implementations/test_local.py000066400000000000000000000033321514661127100255110ustar00rootroot00000000000000import os from pathlib import Path import pytest from upath import UPath from upath.implementations.local import LocalPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base from ..utils import xfail_if_version class TestFSSpecLocal(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): path = f"file://{local_testdir}" self.path = UPath(path) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, LocalPath) @overrides_base def test_cwd(self): # .cwd() is implemented for local filesystems cwd = type(self.path).cwd() assert isinstance(cwd, LocalPath) assert cwd.path == Path.cwd().as_posix() @overrides_base def test_home(self): # .home() is implemented for local filesystems cwd = type(self.path).home() assert isinstance(cwd, LocalPath) assert cwd.path == Path.home().as_posix() @overrides_base def test_chmod(self): # .chmod() works for local filesystems self.path.joinpath("file1.txt").chmod(777) @xfail_if_version("fsspec", lt="2023.10.0", reason="requires fsspec>=2023.10.0") class TestRayIOFSSpecLocal(TestFSSpecLocal): @pytest.fixture(autouse=True) def path(self, local_testdir): path = f"local://{local_testdir}" self.path = UPath(path) @pytest.mark.parametrize( "protocol,path", [ (None, "/tmp/somefile.txt"), ("file", "file:///tmp/somefile.txt"), ("local", "local:///tmp/somefile.txt"), ], ) def test_local_paths_are_pathlike(protocol, path): assert isinstance(UPath(path, protocol=protocol), os.PathLike) universal_pathlib-0.3.10/upath/tests/implementations/test_memory.py000066400000000000000000000021701514661127100257260ustar00rootroot00000000000000import pytest from upath import UPath from upath.implementations.memory import MemoryPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base class TestMemoryPath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): if not local_testdir.startswith("/"): local_testdir = "/" + local_testdir path = f"memory:{local_testdir}" self.path = UPath(path) self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, MemoryPath) @pytest.mark.parametrize( "path, expected", [ ("memory:/", "memory://"), ("memory:/a", "memory://a"), ("memory:/a/b", "memory://a/b"), ("memory://", "memory://"), ("memory://a", "memory://a"), ("memory://a/b", "memory://a/b"), ("memory:///", "memory://"), ("memory:///a", "memory://a"), ("memory:///a/b", "memory://a/b"), ], ) def test_string_representation(path, expected): path = UPath(path) assert str(path) == expected universal_pathlib-0.3.10/upath/tests/implementations/test_s3.py000066400000000000000000000156441514661127100247550ustar00rootroot00000000000000"""see upath/tests/conftest.py for fixtures""" import sys import warnings import fsspec import pytest from upath import UPath from upath.implementations.cloud import S3Path from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base from ..utils import posixify def silence_botocore_datetime_deprecation(cls): # botocore uses datetime.datetime.utcnow in 3.12 which is deprecated # see: https://github.com/boto/boto3/issues/3889#issuecomment-1751296363 if sys.version_info >= (3, 12): return pytest.mark.filterwarnings( "ignore" r":datetime.datetime.utcnow\(\) is deprecated" ":DeprecationWarning" )(cls) else: return cls @silence_botocore_datetime_deprecation class TestUPathS3(BaseTests, metaclass=OverrideMeta): SUPPORTS_EMPTY_DIRS = False @pytest.fixture(autouse=True) def path(self, s3_fixture): path, anon, s3so = s3_fixture self.path = UPath(path, anon=anon, **s3so) self.anon = anon self.s3so = s3so @overrides_base def test_is_correct_class(self): assert isinstance(self.path, S3Path) @extends_base def test_rmdir(self): dirname = "rmdir_test" mock_dir = self.path.joinpath(dirname) mock_dir.joinpath("test.txt").touch() mock_dir.rmdir() assert not mock_dir.exists() with pytest.raises(NotADirectoryError): self.path.joinpath("file1.txt").rmdir() @extends_base def test_relative_to_extra(self): assert "file.txt" == str( UPath("s3://test_bucket/file.txt").relative_to(UPath("s3://test_bucket")) ) @extends_base def test_iterdir_root(self): client_kwargs = self.path.storage_options["client_kwargs"] bucket_path = UPath("s3://other_test_bucket", client_kwargs=client_kwargs) bucket_path.mkdir() (bucket_path / "test1.txt").touch() (bucket_path / "test2.txt").touch() for x in bucket_path.iterdir(): assert x.name != "" assert x.exists() @extends_base @pytest.mark.parametrize( "joiner", [["bucket", "path", "file"], ["bucket/path/file"]] ) def test_no_bucket_joinpath(self, joiner): path = UPath("s3://", anon=self.anon, **self.s3so) path = path.joinpath(*joiner) assert str(path) == "s3://bucket/path/file" @extends_base def test_creating_s3path_with_bucket(self): path = UPath("s3://", bucket="bucket", anon=self.anon, **self.s3so) assert str(path) == "s3://bucket/" @extends_base def test_iterdir_with_plus_in_name(self, s3_with_plus_chr_name): bucket, anon, s3so = s3_with_plus_chr_name p = UPath( f"s3://{bucket}/manual__2022-02-19T14:31:25.891270+00:00", anon=True, **s3so, ) files = list(p.iterdir()) assert len(files) == 1 (file,) = files assert file == p.joinpath("file.txt") @extends_base @pytest.mark.xfail(reason="fsspec/universal_pathlib#144") def test_rglob_with_double_fwd_slash(self, s3_with_double_fwd_slash_files): import boto3 import botocore.exceptions bucket, anon, s3so = s3_with_double_fwd_slash_files conn = boto3.resource("s3", **s3so["client_kwargs"]) # ensure there's no s3://bucket/key.txt object with pytest.raises(botocore.exceptions.ClientError, match=".*Not Found.*"): conn.Object(bucket, "key.txt").load() # ensure there's a s3://bucket//key.txt object assert conn.Object(bucket, "/key.txt").get()["Body"].read() == b"hello world" p0 = UPath(f"s3://{bucket}//key.txt", **s3so) assert p0.read_bytes() == b"hello world" p1 = UPath(f"s3://{bucket}", **s3so) assert list(p1.rglob("*.txt")) == [p0] @pytest.fixture def s3_with_plus_chr_name(s3_server): anon, s3so = s3_server s3 = fsspec.filesystem("s3", anon=False, **s3so) bucket = "plus_chr_bucket" path = f"{bucket}/manual__2022-02-19T14:31:25.891270+00:00" s3.mkdir(path) s3.touch(f"{path}/file.txt") s3.invalidate_cache() try: yield bucket, anon, s3so finally: if s3.exists(bucket): for dir, _, keys in s3.walk(bucket): for key in keys: if key.rstrip("/"): s3.rm(f"{dir}/{key}") @pytest.fixture def s3_with_double_fwd_slash_files(s3_server): anon, s3so = s3_server s3 = fsspec.filesystem("s3", anon=False, **s3so) bucket = "double_fwd_slash_bucket" s3.mkdir(bucket + "/") s3.pipe_file(f"{bucket}//key.txt", b"hello world") try: yield bucket, anon, s3so finally: if s3.exists(bucket): for dir, _, keys in s3.walk(bucket): for key in keys: if key.rstrip("/"): s3.rm(f"{dir}/{key}") def test_path_with_hash_and_space(): assert "with#hash and space" in UPath("s3://bucket/with#hash and space/abc").parts def test_pathlib_consistent_join(): b0 = UPath("s3://mybucket/withkey/").joinpath("subfolder/myfile.txt") b1 = UPath("s3://mybucket/withkey").joinpath("subfolder/myfile.txt") assert b0 == b1 assert "s3://mybucket/withkey/subfolder/myfile.txt" == str(b0) == str(b1) def test_copy__object_key_collides_with_dir_prefix(s3_server, tmp_path): anon, s3so = s3_server s3 = fsspec.filesystem("s3", anon=anon, **{**s3so, "use_listings_cache": False}) bucket = "copy_into_collision_bucket" s3.mkdir(bucket + "/src" + "/common_prefix/") # object under common prefix as key s3.pipe_file(f"{bucket}/src/common_prefix", b"hello world") # store more objects with same prefix s3.pipe_file(f"{bucket}/src/common_prefix/file1.txt", b"1") s3.pipe_file(f"{bucket}/src/common_prefix/file2.txt", b"2") # make sure the sources have a collision assert s3.isdir(f"{bucket}/src/common_prefix") assert s3.isfile(f"{bucket}/src/common_prefix") assert s3.isfile(f"{bucket}/src/common_prefix/file1.txt") assert s3.isfile(f"{bucket}/src/common_prefix/file2.txt") # prepare source and destination src = UPath(f"s3://{bucket}/src", anon=anon, **s3so) dst = UPath(tmp_path) def on_collision_rename_file(src, dst): warnings.warn( f"{src!s} collides with prefix. Renaming target file object to {dst!s}", UserWarning, stacklevel=3, ) return ( dst.with_suffix(dst.suffix + ".COLLISION"), dst, ) # perform copy src.copy_into(dst, on_name_collision=on_collision_rename_file) # check results dst_files = sorted(posixify(x.relative_to(tmp_path)) for x in dst.glob("**/*")) assert dst_files == [ "src", "src/common_prefix", "src/common_prefix.COLLISION", "src/common_prefix/file1.txt", "src/common_prefix/file2.txt", ] universal_pathlib-0.3.10/upath/tests/implementations/test_sftp.py000066400000000000000000000023051514661127100253720ustar00rootroot00000000000000import pytest from upath import UPath from upath.implementations.sftp import SFTPPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base from ..utils import skip_on_windows @skip_on_windows class TestUPathSFTP(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, ssh_fixture): self.path = UPath(ssh_fixture) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, SFTPPath) @pytest.mark.parametrize( "args,parts", [ (("sftp://user@host",), ("/",)), (("sftp://user@host/",), ("/",)), (("sftp://user@host", ""), ("/",)), (("sftp://user@host/", ""), ("/",)), (("sftp://user@host", "/"), ("/",)), (("sftp://user@host/", "/"), ("/",)), (("sftp://user@host/abc",), ("/", "abc")), (("sftp://user@host", "abc"), ("/", "abc")), (("sftp://user@host", "/abc"), ("/", "abc")), (("sftp://user@host/", "/abc"), ("/", "abc")), ], ) def test_join_produces_correct_parts(args, parts): pth = UPath(*args) assert pth.storage_options == {"host": "host", "username": "user"} assert pth.parts == parts universal_pathlib-0.3.10/upath/tests/implementations/test_smb.py000066400000000000000000000016571514661127100252100ustar00rootroot00000000000000import pytest from upath import UPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import overrides_base from ..utils import skip_on_windows @skip_on_windows class TestUPathSMB(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, smb_fixture): self.path = UPath(smb_fixture) @overrides_base def test_is_correct_class(self): from upath.implementations.smb import SMBPath assert isinstance(self.path, SMBPath) @overrides_base @pytest.mark.parametrize( "pattern", ( "*.txt", pytest.param( "*", marks=pytest.mark.xfail( reason="SMBFileSystem.info appends '/' to dirs" ), ), "**/*.txt", ), ) def test_glob(self, pathlib_base, pattern): super().test_glob(pathlib_base, pattern) universal_pathlib-0.3.10/upath/tests/implementations/test_tar.py000066400000000000000000000027331514661127100252110ustar00rootroot00000000000000import tarfile import pytest from upath import UPath from upath.implementations.tar import TarPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import overrides_base @pytest.fixture(scope="function") def tarred_testdir_file(local_testdir, tmp_path_factory): base = tmp_path_factory.mktemp("tarpath") tar_path = base / "test.tar" with tarfile.TarFile(tar_path, "w") as tf: tf.add(local_testdir, arcname="", recursive=True) return str(tar_path) class TestTarPath( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): @pytest.fixture(autouse=True) def path(self, tarred_testdir_file): self.path = UPath("tar://", fo=tarred_testdir_file) # self.prepare_file_system() done outside of UPath @overrides_base def test_is_correct_class(self): assert isinstance(self.path, TarPath) @pytest.fixture(scope="function") def tarred_testdir_file_in_memory(tarred_testdir_file, clear_fsspec_memory_cache): p = UPath(tarred_testdir_file, protocol="file") t = p.move(UPath("memory:///mytarfile.tar")) assert t.protocol == "memory" assert t.exists() yield t.as_uri() class TestChainedTarPath(TestTarPath): @pytest.fixture(autouse=True) def path(self, tarred_testdir_file_in_memory): self.path = UPath("tar://::memory:///mytarfile.tar") universal_pathlib-0.3.10/upath/tests/implementations/test_webdav.py000066400000000000000000000032341514661127100256700ustar00rootroot00000000000000from pathlib import Path import pytest from upath import UPath from upath.implementations.webdav import WebdavPath from ..cases import BaseTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base class TestUPathWebdav(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True, scope="function") def path(self, webdav_fixture): self.path = UPath(webdav_fixture, auth=("USER", "PASSWORD")) @overrides_base def test_is_correct_class(self): assert isinstance(self.path, WebdavPath) @extends_base def test_storage_options_base_url(self): # ensure that base_url is correct base_url = self.path.storage_options["base_url"] assert base_url == self.path.fs.client.base_url @overrides_base @pytest.mark.parametrize( "target_factory", [ lambda obj, name: str(obj.joinpath(name).absolute()), pytest.param( lambda obj, name: UPath(obj.absolute().joinpath(name).path), marks=pytest.mark.xfail(reason="webdav has no root..."), ), pytest.param( lambda obj, name: Path(obj.absolute().joinpath(name).path), marks=pytest.mark.xfail(reason="webdav has no root..."), ), lambda obj, name: obj.absolute().joinpath(name), ], ids=[ "str_absolute", "plain_upath_absolute", "plain_path_absolute", "self_upath_absolute", ], ) def test_rename_with_target_absolute(self, target_factory): super().test_rename_with_target_absolute(target_factory) universal_pathlib-0.3.10/upath/tests/implementations/test_zip.py000066400000000000000000000054571514661127100252330ustar00rootroot00000000000000import os import zipfile import pytest from upath import UnsupportedOperation from upath import UPath from upath.implementations.zip import ZipPath from ..cases import JoinablePathTests from ..cases import NonWritablePathTests from ..cases import ReadablePathTests from ..utils import OverrideMeta from ..utils import extends_base from ..utils import overrides_base @pytest.fixture(scope="function") def zipped_testdir_file(local_testdir, tmp_path_factory): base = tmp_path_factory.mktemp("zippath") zip_path = base / "test.zip" with zipfile.ZipFile(zip_path, "w") as zf: for root, _, files in os.walk(local_testdir): for file in files: full_path = os.path.join(root, file) arcname = os.path.relpath(full_path, start=local_testdir) zf.write(full_path, arcname=arcname) return str(zip_path) @pytest.fixture(scope="function") def empty_zipped_testdir_file(tmp_path): tmp_path = tmp_path.joinpath("zippath") tmp_path.mkdir() zip_path = tmp_path / "test.zip" with zipfile.ZipFile(zip_path, "w"): pass return str(zip_path) class TestZipPath( JoinablePathTests, ReadablePathTests, NonWritablePathTests, metaclass=OverrideMeta, ): @pytest.fixture(autouse=True) def path(self, zipped_testdir_file, request): try: (mode,) = request.param except (ValueError, TypeError, AttributeError): mode = "r" self.path = UPath("zip://", fo=zipped_testdir_file, mode=mode) try: yield finally: self.path.fs.clear_instance_cache() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, ZipPath) @extends_base def test_write_mode_is_disabled(self, tmp_path): with pytest.raises(UnsupportedOperation): UPath("zip://", fo=tmp_path.joinpath("myzip.zip"), mode="a") with pytest.raises(UnsupportedOperation): UPath("zip://", fo=tmp_path.joinpath("myzip.zip"), mode="x") with pytest.raises(UnsupportedOperation): UPath("zip://", fo=tmp_path.joinpath("myzip.zip"), mode="w") @pytest.fixture(scope="function") def zipped_testdir_file_in_memory(zipped_testdir_file, clear_fsspec_memory_cache): p = UPath(zipped_testdir_file, protocol="file") t = p.move(UPath("memory:///myzipfile.zip")) assert t.protocol == "memory" assert t.exists() yield t.as_uri() class TestChainedZipPath(TestZipPath): @pytest.fixture(autouse=True) def path(self, zipped_testdir_file_in_memory, request): try: (mode,) = request.param except (ValueError, TypeError, AttributeError): mode = "r" self.path = UPath( "zip://", fo="/myzipfile.zip", mode=mode, target_protocol="memory" ) universal_pathlib-0.3.10/upath/tests/pathlib/000077500000000000000000000000001514661127100212205ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/pathlib/__init__.py000066400000000000000000000000001514661127100233170ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/pathlib/_test_support.py000066400000000000000000000363111514661127100245100ustar00rootroot00000000000000"""vendoring everything needed to run test_pathlib.py https://github.com/python/cpython/tree/10bf2cd404320252ef162d5699cb7ce52a970d44 from test.support import import_helper from test.support import set_recursion_limit from test.support import is_emscripten, is_wasi from test.support import os_helper from test.support.os_helper import TESTFN, FakePath """ import os import stat import string import sys import collections.abc import contextlib import time import warnings from types import SimpleNamespace import pytest from pytest import importorskip import_helper = SimpleNamespace(import_module=importorskip) import_module = importorskip @contextlib.contextmanager def set_recursion_limit(limit): """Temporarily change the recursion limit.""" original_limit = sys.getrecursionlimit() try: sys.setrecursionlimit(limit) yield finally: sys.setrecursionlimit(original_limit) is_emscripten = sys.platform == "emscripten" is_wasi = sys.platform == "wasi" class FakePath: """Simple implementation of the path protocol.""" def __init__(self, path): self.path = path def __repr__(self): return f"" def __fspath__(self): if ( isinstance(self.path, BaseException) or isinstance(self.path, type) and issubclass(self.path, BaseException) ): raise self.path else: return self.path class EnvironmentVarGuard(collections.abc.MutableMapping): """Class to help protect the environment variable properly. Can be used as a context manager.""" def __init__(self): self._environ = os.environ self._changed = {} def __getitem__(self, envvar): return self._environ[envvar] def __setitem__(self, envvar, value): # Remember the initial value on the first access if envvar not in self._changed: self._changed[envvar] = self._environ.get(envvar) self._environ[envvar] = value def __delitem__(self, envvar): # Remember the initial value on the first access if envvar not in self._changed: self._changed[envvar] = self._environ.get(envvar) if envvar in self._environ: del self._environ[envvar] def keys(self): return self._environ.keys() def __iter__(self): return iter(self._environ) def __len__(self): return len(self._environ) def set(self, envvar, value): self[envvar] = value def unset(self, envvar): del self[envvar] def copy(self): # We do what os.environ.copy() does. return dict(self) def __enter__(self): return self def __exit__(self, *ignore_exc): for k, v in self._changed.items(): if v is None: if k in self._environ: del self._environ[k] else: self._environ[k] = v os.environ = self._environ # Filename used for testing TESTFN_ASCII = "@test" # Disambiguate TESTFN for parallel testing, while letting it remain a valid # module name. TESTFN_ASCII = "{}_{}_tmp".format(TESTFN_ASCII, os.getpid()) # FS_NONASCII: non-ASCII character encodable by os.fsencode(), # or an empty string if there is no such character. FS_NONASCII = "" for character in ( # First try printable and common characters to have a readable filename. # For each character, the encoding list are just example of encodings able # to encode the character (the list is not exhaustive). # U+00E6 (Latin Small Letter Ae): cp1252, iso-8859-1 "\u00E6", # U+0130 (Latin Capital Letter I With Dot Above): cp1254, iso8859_3 "\u0130", # U+0141 (Latin Capital Letter L With Stroke): cp1250, cp1257 "\u0141", # U+03C6 (Greek Small Letter Phi): cp1253 "\u03C6", # U+041A (Cyrillic Capital Letter Ka): cp1251 "\u041A", # U+05D0 (Hebrew Letter Alef): Encodable to cp424 "\u05D0", # U+060C (Arabic Comma): cp864, cp1006, iso8859_6, mac_arabic "\u060C", # U+062A (Arabic Letter Teh): cp720 "\u062A", # U+0E01 (Thai Character Ko Kai): cp874 "\u0E01", # Then try more "special" characters. "special" because they may be # interpreted or displayed differently depending on the exact locale # encoding and the font. # U+00A0 (No-Break Space) "\u00A0", # U+20AC (Euro Sign) "\u20AC", ): try: # If Python is set up to use the legacy 'mbcs' in Windows, # 'replace' error mode is used, and encode() returns b'?' # for characters missing in the ANSI codepage if os.fsdecode(os.fsencode(character)) != character: raise UnicodeError except UnicodeError: pass else: FS_NONASCII = character break # TESTFN_UNDECODABLE is a filename (bytes type) that should *not* be able to be # decoded from the filesystem encoding (in strict mode). It can be None if we # cannot generate such filename (ex: the latin1 encoding can decode any byte # sequence). On UNIX, TESTFN_UNDECODABLE can be decoded by os.fsdecode() thanks # to the surrogateescape error handler (PEP 383), but not from the filesystem # encoding in strict mode. TESTFN_UNDECODABLE = None for name in ( # b'\xff' is not decodable by os.fsdecode() with code page 932. Windows # accepts it to create a file or a directory, or don't accept to enter to # such directory (when the bytes name is used). So test b'\xe7' first: # it is not decodable from cp932. b"\xe7w\xf0", # undecodable from ASCII, UTF-8 b"\xff", # undecodable from iso8859-3, iso8859-6, iso8859-7, cp424, iso8859-8, cp856 # and cp857 b"\xae\xd5" # undecodable from UTF-8 (UNIX and Mac OS X) b"\xed\xb2\x80", b"\xed\xb4\x80", # undecodable from shift_jis, cp869, cp874, cp932, cp1250, cp1251, cp1252, # cp1253, cp1254, cp1255, cp1257, cp1258 b"\x81\x98", ): try: name.decode(sys.getfilesystemencoding()) except UnicodeDecodeError: try: name.decode( sys.getfilesystemencoding(), sys.getfilesystemencodeerrors() ) except UnicodeDecodeError: continue TESTFN_UNDECODABLE = os.fsencode(TESTFN_ASCII) + name break if FS_NONASCII: TESTFN_NONASCII = TESTFN_ASCII + FS_NONASCII else: TESTFN_NONASCII = None TESTFN = TESTFN_NONASCII or TESTFN_ASCII _can_symlink = None def can_symlink(): global _can_symlink if _can_symlink is not None: return _can_symlink # WASI / wasmtime prevents symlinks with absolute paths, see man # openat2(2) RESOLVE_BENEATH. Almost all symlink tests use absolute # paths. Skip symlink tests on WASI for now. src = os.path.abspath(TESTFN) symlink_path = src + "can_symlink" try: os.symlink(src, symlink_path) can = True except (OSError, NotImplementedError, AttributeError): can = False else: os.remove(symlink_path) _can_symlink = can return can def skip_unless_symlink(test): """Skip decorator for tests that require functional symlink""" ok = can_symlink() msg = "Requires functional symlink implementation" return test if ok else pytest.mark.skip(msg)(test) _can_chmod = None def can_chmod(): global _can_chmod if _can_chmod is not None: return _can_chmod if not hasattr(os, "chown"): _can_chmod = False return _can_chmod try: with open(TESTFN, "wb") as _: try: os.chmod(TESTFN, 0o777) mode1 = os.stat(TESTFN).st_mode os.chmod(TESTFN, 0o666) mode2 = os.stat(TESTFN).st_mode except OSError: can = False else: can = stat.S_IMODE(mode1) != stat.S_IMODE(mode2) finally: unlink(TESTFN) _can_chmod = can return can def skip_unless_working_chmod(test): """Skip tests that require working os.chmod() WASI SDK 15.0 cannot change file mode bits. """ ok = can_chmod() msg = "requires working os.chmod()" return test if ok else pytest.mark.skip(msg)(test) def unlink(filename): try: _unlink(filename) except (FileNotFoundError, NotADirectoryError): pass def _force_run(path, func, *args): try: return func(*args) except FileNotFoundError: # chmod() won't fix a missing file. raise except OSError: os.chmod(path, stat.S_IRWXU) return func(*args) if sys.platform.startswith("win"): def _waitfor(func, pathname, waitall=False): # Perform the operation func(pathname) # Now setup the wait loop if waitall: dirname = pathname else: dirname, name = os.path.split(pathname) dirname = dirname or '.' # Check for `pathname` to be removed from the filesystem. # The exponential backoff of the timeout amounts to a total # of ~1 second after which the deletion is probably an error # anyway. # Testing on an i7@4.3GHz shows that usually only 1 iteration is # required when contention occurs. timeout = 0.001 while timeout < 1.0: # Note we are only testing for the existence of the file(s) in # the contents of the directory regardless of any security or # access rights. If we have made it this far, we have sufficient # permissions to do that much using Python's equivalent of the # Windows API FindFirstFile. # Other Windows APIs can fail or give incorrect results when # dealing with files that are pending deletion. L = os.listdir(dirname) if not (L if waitall else name in L): return # Increase the timeout and try again time.sleep(timeout) timeout *= 2 warnings.warn('tests may fail, delete still pending for ' + pathname, RuntimeWarning, stacklevel=4) def _unlink(filename): _waitfor(os.unlink, filename) def _rmdir(dirname): _waitfor(os.rmdir, dirname) def _rmtree(path): def _rmtree_inner(path): for name in _force_run(path, os.listdir, path): fullname = os.path.join(path, name) try: mode = os.lstat(fullname).st_mode except OSError as exc: print("support.rmtree(): os.lstat(%r) failed with %s" % (fullname, exc), file=sys.__stderr__) mode = 0 if stat.S_ISDIR(mode): _waitfor(_rmtree_inner, fullname, waitall=True) _force_run(fullname, os.rmdir, fullname) else: _force_run(fullname, os.unlink, fullname) _waitfor(_rmtree_inner, path, waitall=True) _waitfor(lambda p: _force_run(p, os.rmdir, p), path) def _longpath(path): try: import ctypes except ImportError: # No ctypes means we can't expands paths. pass else: buffer = ctypes.create_unicode_buffer(len(path) * 2) length = ctypes.windll.kernel32.GetLongPathNameW(path, buffer, len(buffer)) if length: return buffer[:length] return path else: _unlink = os.unlink _rmdir = os.rmdir def _rmtree(path): import shutil try: shutil.rmtree(path) return except OSError: pass def _rmtree_inner(path): for name in _force_run(path, os.listdir, path): fullname = os.path.join(path, name) try: mode = os.lstat(fullname).st_mode except OSError: mode = 0 if stat.S_ISDIR(mode): _rmtree_inner(fullname) _force_run(path, os.rmdir, fullname) else: _force_run(path, os.unlink, fullname) _rmtree_inner(path) os.rmdir(path) def _longpath(path): return path def rmdir(dirname): try: _rmdir(dirname) except FileNotFoundError: pass def rmtree(path): try: _rmtree(path) except FileNotFoundError: pass def fs_is_case_insensitive(directory): """Detects if the file system for the specified directory is case-insensitive.""" import tempfile with tempfile.NamedTemporaryFile(dir=directory) as base: base_path = base.name case_path = base_path.upper() if case_path == base_path: case_path = base_path.lower() try: return os.path.samefile(base_path, case_path) except FileNotFoundError: return False @contextlib.contextmanager def change_cwd(path, quiet=False): """Return a context manager that changes the current working directory. Arguments: path: the directory to use as the temporary current working directory. quiet: if False (the default), the context manager raises an exception on error. Otherwise, it issues only a warning and keeps the current working directory the same. """ saved_dir = os.getcwd() try: os.chdir(os.path.realpath(path)) except OSError as exc: if not quiet: raise warnings.warn( f"tests may fail, unable to change the current working " f"directory to {path!r}: {exc}", RuntimeWarning, stacklevel=3, ) try: yield os.getcwd() finally: os.chdir(saved_dir) try: import ctypes kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) ERROR_FILE_NOT_FOUND = 2 DDD_REMOVE_DEFINITION = 2 DDD_EXACT_MATCH_ON_REMOVE = 4 DDD_NO_BROADCAST_SYSTEM = 8 except (ImportError, AttributeError): @contextlib.contextmanager def subst_drive(path): raise pytest.skip("ctypes or kernel32 is not available") else: @contextlib.contextmanager def subst_drive(path): """Temporarily yield a substitute drive for a given path.""" for c in reversed(string.ascii_uppercase): drive = f"{c}:" if ( not kernel32.QueryDosDeviceW(drive, None, 0) and ctypes.get_last_error() == ERROR_FILE_NOT_FOUND ): break else: raise pytest.skip("no available logical drive") if not kernel32.DefineDosDeviceW(DDD_NO_BROADCAST_SYSTEM, drive, path): raise ctypes.WinError(ctypes.get_last_error()) try: yield drive finally: if not kernel32.DefineDosDeviceW( DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE, drive, path ): raise ctypes.WinError(ctypes.get_last_error()) os_helper = SimpleNamespace( rmtree=rmtree, can_symlink=can_symlink, skip_unless_symlink=skip_unless_symlink, TESTFN=TESTFN, _longpath=_longpath, EnvironmentVarGuard=EnvironmentVarGuard, skip_unless_working_chmod=skip_unless_working_chmod, fs_is_case_insensitive=fs_is_case_insensitive, change_cwd=change_cwd, subst_drive=subst_drive, ) universal_pathlib-0.3.10/upath/tests/pathlib/conftest.py000066400000000000000000000023271514661127100234230ustar00rootroot00000000000000import sys import pytest BASE_URL = "https://raw.githubusercontent.com/python/cpython/{}/Lib/test/test_pathlib.py" # noqa # current origin of pathlib tests: TEST_FILES = { "test_pathlib_38.py": "7475aa2c590e33a47f5e79e4079bca0645e93f2f", "test_pathlib_39.py": "d718764f389acd1bf4a5a65661bb58862f14fb98", "test_pathlib_310.py": "b382bf50c53e6eab09f3e3bf0802ab052cb0289d", "test_pathlib_311.py": "846a23d0b8f08e62a90682c51ce01301eb923f2e", "test_pathlib_312.py": "97a6a418167f1c8bbb014fab813e440b88cf2221", # 3.12.0b4 } def pytest_ignore_collect(collection_path): """prevents pathlib tests from other python version than the current to be collected (otherwise we see a lot of skipped tests in the pytest output) """ v2 = sys.version_info[:2] return { "test_pathlib_38.py": v2 != (3, 8), "test_pathlib_39.py": v2 != (3, 9), "test_pathlib_310.py": v2 != (3, 10), "test_pathlib_311.py": v2 != (3, 11), "test_pathlib_312.py": v2 != (3, 12), }.get(collection_path.name, False) def pytest_collection_modifyitems(config, items): """mark all tests in this folder as pathlib tests""" for item in items: item.add_marker(pytest.mark.pathlib) universal_pathlib-0.3.10/upath/tests/pathlib/test_pathlib_310.py000066400000000000000000003133471514661127100246520ustar00rootroot00000000000000import collections.abc import io import os import sys import errno import pathlib import pickle import socket import stat import tempfile import unittest from unittest import mock from ._test_support import import_helper from . import _test_support as os_helper from ._test_support import TESTFN, FakePath try: import grp, pwd except ImportError: grp = pwd = None from upath.core import UPath from upath.implementations.local import PosixUPath, WindowsUPath import pytest pytestmark = pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="py310 only") # # Tests for the pure classes. # class _BasePurePathTest(object): # Keys are canonical paths, values are list of tuples of arguments # supposed to produce equal paths. equivalences = { 'a/b': [ ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'), ('a/b/',), ('a//b',), ('a//b//',), # Empty components get removed. ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''), ], '/b/c/d': [ ('a', '/b/c', 'd'), ('a', '///b//c', 'd/'), ('/a', '/b/c', 'd'), # Empty components get removed. ('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'), ], } def setUp(self): p = self.cls('a') self.flavour = p._flavour self.sep = self.flavour.sep self.altsep = self.flavour.altsep def test_constructor_common(self): P = self.cls p = P('a') self.assertIsInstance(p, P) P('a', 'b', 'c') P('/a', 'b', 'c') P('a/b/c') P('/a/b/c') P(FakePath("a/b/c")) self.assertEqual(P(P('a')), P('a')) self.assertEqual(P(P('a'), 'b'), P('a/b')) self.assertEqual(P(P('a'), P('b')), P('a/b')) self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c"))) def _check_str_subclass(self, *args): # Issue #21127: it should be possible to construct a PurePath object # from a str subclass instance, and it then gets converted to # a pure str object. class StrSubclass(str): pass P = self.cls p = P(*(StrSubclass(x) for x in args)) self.assertEqual(p, P(*args)) for part in p.parts: self.assertIs(type(part), str) def test_str_subclass_common(self): self._check_str_subclass('') self._check_str_subclass('.') self._check_str_subclass('a') self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') def test_join_common(self): P = self.cls p = P('a/b') pp = p.joinpath('c') self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p.joinpath('c', 'd') self.assertEqual(pp, P('a/b/c/d')) pp = p.joinpath(P('c')) self.assertEqual(pp, P('a/b/c')) pp = p.joinpath('/c') self.assertEqual(pp, P('/c')) def test_div_common(self): # Basically the same as joinpath(). P = self.cls p = P('a/b') pp = p / 'c' self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p / 'c/d' self.assertEqual(pp, P('a/b/c/d')) pp = p / 'c' / 'd' self.assertEqual(pp, P('a/b/c/d')) pp = 'c' / p / 'd' self.assertEqual(pp, P('c/a/b/d')) pp = p / P('c') self.assertEqual(pp, P('a/b/c')) pp = p/ '/c' self.assertEqual(pp, P('/c')) def _check_str(self, expected, args): p = self.cls(*args) self.assertEqual(str(p), expected.replace('/', self.sep)) def test_str_common(self): # Canonicalized paths roundtrip. for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self._check_str(pathstr, (pathstr,)) # Special case for the empty path. self._check_str('.', ('',)) # Other tests for str() are in test_equivalences(). def test_as_posix_common(self): P = self.cls for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self.assertEqual(P(pathstr).as_posix(), pathstr) # Other tests for as_posix() are in test_equivalences(). def test_as_bytes_common(self): sep = os.fsencode(self.sep) P = self.cls self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b') def test_as_uri_common(self): P = self.cls with self.assertRaises(ValueError): P('a').as_uri() with self.assertRaises(ValueError): P().as_uri() def test_repr_common(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): p = self.cls(pathstr) clsname = p.__class__.__name__ r = repr(p) # The repr() is in the form ClassName("forward-slashes path"). self.assertTrue(r.startswith(clsname + '('), r) self.assertTrue(r.endswith(')'), r) inner = r[len(clsname) + 1 : -1] self.assertEqual(eval(inner), p.as_posix()) # The repr() roundtrips. q = eval(r, {"PosixUPath": PosixUPath, "WindowsUPath": WindowsUPath}) self.assertIs(q.__class__, p.__class__) self.assertEqual(q, p) self.assertEqual(repr(q), r) def test_eq_common(self): P = self.cls self.assertEqual(P('a/b'), P('a/b')) self.assertEqual(P('a/b'), P('a', 'b')) self.assertNotEqual(P('a/b'), P('a')) self.assertNotEqual(P('a/b'), P('/a/b')) self.assertNotEqual(P('a/b'), P()) self.assertNotEqual(P('/a/b'), P('/')) self.assertNotEqual(P(), P('/')) self.assertNotEqual(P(), "") self.assertNotEqual(P(), {}) self.assertNotEqual(P(), int) def test_match_common(self): P = self.cls self.assertRaises(ValueError, P('a').match, '') self.assertRaises(ValueError, P('a').match, '.') # Simple relative pattern. self.assertTrue(P('b.py').match('b.py')) self.assertTrue(P('a/b.py').match('b.py')) self.assertTrue(P('/a/b.py').match('b.py')) self.assertFalse(P('a.py').match('b.py')) self.assertFalse(P('b/py').match('b.py')) self.assertFalse(P('/a.py').match('b.py')) self.assertFalse(P('b.py/c').match('b.py')) # Wildcard relative pattern. self.assertTrue(P('b.py').match('*.py')) self.assertTrue(P('a/b.py').match('*.py')) self.assertTrue(P('/a/b.py').match('*.py')) self.assertFalse(P('b.pyc').match('*.py')) self.assertFalse(P('b./py').match('*.py')) self.assertFalse(P('b.py/c').match('*.py')) # Multi-part relative pattern. self.assertTrue(P('ab/c.py').match('a*/*.py')) self.assertTrue(P('/d/ab/c.py').match('a*/*.py')) self.assertFalse(P('a.py').match('a*/*.py')) self.assertFalse(P('/dab/c.py').match('a*/*.py')) self.assertFalse(P('ab/c.py/d').match('a*/*.py')) # Absolute pattern. self.assertTrue(P('/b.py').match('/*.py')) self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('a/b.py').match('/*.py')) self.assertFalse(P('/a/b.py').match('/*.py')) # Multi-part absolute pattern. self.assertTrue(P('/a/b.py').match('/a/*.py')) self.assertFalse(P('/ab.py').match('/a/*.py')) self.assertFalse(P('/a/b/c.py').match('/a/*.py')) # Multi-part glob-style pattern. self.assertFalse(P('/a/b/c.py').match('/**/*.py')) self.assertTrue(P('/a/b/c.py').match('/a/**/*.py')) def test_ordering_common(self): # Ordering is tuple-alike. def assertLess(a, b): self.assertLess(a, b) self.assertGreater(b, a) P = self.cls a = P('a') b = P('a/b') c = P('abc') d = P('b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) P = self.cls a = P('/a') b = P('/a/b') c = P('/abc') d = P('/b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) with self.assertRaises(TypeError): P() < {} def test_parts_common(self): # `parts` returns a tuple. sep = self.sep P = self.cls p = P('a/b') parts = p.parts self.assertEqual(parts, ('a', 'b')) # The object gets reused. self.assertIs(parts, p.parts) # When the path is absolute, the anchor is a separate part. p = P('/a/b') parts = p.parts self.assertEqual(parts, (sep, 'a', 'b')) def test_fspath_common(self): P = self.cls p = P('a/b') self._check_str(p.__fspath__(), ('a/b',)) self._check_str(os.fspath(p), ('a/b',)) def test_equivalences(self): for k, tuples in self.equivalences.items(): canon = k.replace('/', self.sep) posix = k.replace(self.sep, '/') if canon != posix: tuples = tuples + [ tuple(part.replace('/', self.sep) for part in t) for t in tuples ] tuples.append((posix, )) pcanon = self.cls(canon) for t in tuples: p = self.cls(*t) self.assertEqual(p, pcanon, "failed with args {}".format(t)) self.assertEqual(hash(p), hash(pcanon)) self.assertEqual(str(p), canon) self.assertEqual(p.as_posix(), posix) def test_parent_common(self): # Relative P = self.cls p = P('a/b/c') self.assertEqual(p.parent, P('a/b')) self.assertEqual(p.parent.parent, P('a')) self.assertEqual(p.parent.parent.parent, P()) self.assertEqual(p.parent.parent.parent.parent, P()) # Anchored p = P('/a/b/c') self.assertEqual(p.parent, P('/a/b')) self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative P = self.cls p = P('a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('a/b')) self.assertEqual(par[1], P('a')) self.assertEqual(par[2], P('.')) self.assertEqual(par[-1], P('.')) self.assertEqual(par[-2], P('a')) self.assertEqual(par[-3], P('a/b')) self.assertEqual(par[0:1], (P('a/b'),)) self.assertEqual(par[:2], (P('a/b'), P('a'))) self.assertEqual(par[:-1], (P('a/b'), P('a'))) self.assertEqual(par[1:], (P('a'), P('.'))) self.assertEqual(par[::2], (P('a/b'), P('.'))) self.assertEqual(par[::-1], (P('.'), P('a'), P('a/b'))) self.assertEqual(list(par), [P('a/b'), P('a'), P('.')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] with self.assertRaises(TypeError): par[0] = p # Anchored p = P('/a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('/a/b')) self.assertEqual(par[1], P('/a')) self.assertEqual(par[2], P('/')) self.assertEqual(par[-1], P('/')) self.assertEqual(par[-2], P('/a')) self.assertEqual(par[-3], P('/a/b')) self.assertEqual(par[0:1], (P('/a/b'),)) self.assertEqual(par[:2], (P('/a/b'), P('/a'))) self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) self.assertEqual(par[1:], (P('/a'), P('/'))) self.assertEqual(par[::2], (P('/a/b'), P('/'))) self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] def test_drive_common(self): P = self.cls self.assertEqual(P('a/b').drive, '') self.assertEqual(P('/a/b').drive, '') self.assertEqual(P('').drive, '') def test_root_common(self): P = self.cls sep = self.sep self.assertEqual(P('').root, '') self.assertEqual(P('a/b').root, '') self.assertEqual(P('/').root, sep) self.assertEqual(P('/a/b').root, sep) def test_anchor_common(self): P = self.cls sep = self.sep self.assertEqual(P('').anchor, '') self.assertEqual(P('a/b').anchor, '') self.assertEqual(P('/').anchor, sep) self.assertEqual(P('/a/b').anchor, sep) def test_name_common(self): P = self.cls self.assertEqual(P('').name, '') self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') self.assertEqual(P('/a/b').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') def test_suffix_common(self): P = self.cls self.assertEqual(P('').suffix, '') self.assertEqual(P('.').suffix, '') self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') self.assertEqual(P('/a/b').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') self.assertEqual(P('a/.hgrc').suffix, '') self.assertEqual(P('/a/.hgrc').suffix, '') self.assertEqual(P('a/.hg.rc').suffix, '.rc') self.assertEqual(P('/a/.hg.rc').suffix, '.rc') self.assertEqual(P('a/b.tar.gz').suffix, '.gz') self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') def test_suffixes_common(self): P = self.cls self.assertEqual(P('').suffixes, []) self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) self.assertEqual(P('a/.hgrc').suffixes, []) self.assertEqual(P('/a/.hgrc').suffixes, []) self.assertEqual(P('a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) def test_stem_common(self): P = self.cls self.assertEqual(P('').stem, '') self.assertEqual(P('.').stem, '') self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name_common(self): P = self.cls self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') self.assertRaises(ValueError, P('a/b').with_name, '') self.assertRaises(ValueError, P('a/b').with_name, '/c') self.assertRaises(ValueError, P('a/b').with_name, 'c/') self.assertRaises(ValueError, P('a/b').with_name, 'c/d') def test_with_stem_common(self): P = self.cls self.assertEqual(P('a/b').with_stem('d'), P('a/d')) self.assertEqual(P('/a/b').with_stem('d'), P('/a/d')) self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py')) self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py')) self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') self.assertRaises(ValueError, P('a/b').with_stem, '') self.assertRaises(ValueError, P('a/b').with_stem, '/c') self.assertRaises(ValueError, P('a/b').with_stem, 'c/') self.assertRaises(ValueError, P('a/b').with_stem, 'c/d') def test_with_suffix_common(self): P = self.cls self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('a/b').with_suffix, '/') self.assertRaises(ValueError, P('a/b').with_suffix, '.') self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d') self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(ValueError, P('a/b').with_suffix, (self.flavour.sep, 'd')) def test_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.relative_to) self.assertRaises(TypeError, p.relative_to, b'a') self.assertEqual(p.relative_to(P()), P('a/b')) self.assertEqual(p.relative_to(''), P('a/b')) self.assertEqual(p.relative_to(P('a')), P('b')) self.assertEqual(p.relative_to('a'), P('b')) self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to('a/b'), P()) # With several args. self.assertEqual(p.relative_to('a', 'b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('c')) self.assertRaises(ValueError, p.relative_to, P('a/b/c')) self.assertRaises(ValueError, p.relative_to, P('a/c')) self.assertRaises(ValueError, p.relative_to, P('/a')) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) self.assertEqual(p.relative_to(P('/a')), P('b')) self.assertEqual(p.relative_to('/a'), P('b')) self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to('/a/b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) self.assertRaises(ValueError, p.relative_to, P('/a/c')) self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('a')) def test_is_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.is_relative_to) self.assertRaises(TypeError, p.is_relative_to, b'a') self.assertTrue(p.is_relative_to(P())) self.assertTrue(p.is_relative_to('')) self.assertTrue(p.is_relative_to(P('a'))) self.assertTrue(p.is_relative_to('a/')) self.assertTrue(p.is_relative_to(P('a/b'))) self.assertTrue(p.is_relative_to('a/b')) # With several args. self.assertTrue(p.is_relative_to('a', 'b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('c'))) self.assertFalse(p.is_relative_to(P('a/b/c'))) self.assertFalse(p.is_relative_to(P('a/c'))) self.assertFalse(p.is_relative_to(P('/a'))) p = P('/a/b') self.assertTrue(p.is_relative_to(P('/'))) self.assertTrue(p.is_relative_to('/')) self.assertTrue(p.is_relative_to(P('/a'))) self.assertTrue(p.is_relative_to('/a')) self.assertTrue(p.is_relative_to('/a/')) self.assertTrue(p.is_relative_to(P('/a/b'))) self.assertTrue(p.is_relative_to('/a/b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/c'))) self.assertFalse(p.is_relative_to(P('/a/b/c'))) self.assertFalse(p.is_relative_to(P('/a/c'))) self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) def test_pickling_common(self): P = self.cls p = P('/a/b') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertIs(pp.__class__, p.__class__) self.assertEqual(pp, p) self.assertEqual(hash(pp), hash(p)) self.assertEqual(str(pp), str(p)) class PurePosixPathTest(_BasePurePathTest): cls = pathlib.PurePosixPath def test_root(self): P = self.cls self.assertEqual(P('/a/b').root, '/') self.assertEqual(P('///a/b').root, '/') # POSIX special case for two leading slashes. self.assertEqual(P('//a/b').root, '//') def test_eq(self): P = self.cls self.assertNotEqual(P('a/b'), P('A/b')) self.assertEqual(P('/a'), P('///a')) self.assertNotEqual(P('/a'), P('//a')) def test_as_uri(self): P = self.cls self.assertEqual(P('/').as_uri(), 'file:///') self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c') self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c') def test_as_uri_non_ascii(self): from urllib.parse import quote_from_bytes P = self.cls try: os.fsencode('\xe9') except UnicodeEncodeError: self.skipTest("\\xe9 cannot be encoded to the filesystem encoding") self.assertEqual(P('/a/b\xe9').as_uri(), 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9'))) def test_match(self): P = self.cls self.assertFalse(P('A.py').match('a.PY')) def test_is_absolute(self): P = self.cls self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertTrue(P('/').is_absolute()) self.assertTrue(P('/a').is_absolute()) self.assertTrue(P('/a/b/').is_absolute()) self.assertTrue(P('//a').is_absolute()) self.assertTrue(P('//a/b').is_absolute()) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved()) def test_join(self): P = self.cls p = P('//a') pp = p.joinpath('b') self.assertEqual(pp, P('//a/b')) pp = P('/a').joinpath('//c') self.assertEqual(pp, P('//c')) pp = P('//a').joinpath('/c') self.assertEqual(pp, P('/c')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) class PureWindowsPathTest(_BasePurePathTest): cls = pathlib.PureWindowsPath equivalences = _BasePurePathTest.equivalences.copy() equivalences.update({ 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ], 'c:/a': [ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'), ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'), ], '//a/b/': [ ('//a/b',) ], '//a/b/c': [ ('//a/b', 'c'), ('//a/b/', 'c'), ], }) def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') p = self.cls('c:/a/b/c') self.assertEqual(str(p), 'c:\\a\\b\\c') p = self.cls('//a/b') self.assertEqual(str(p), '\\\\a\\b\\') p = self.cls('//a/b/c') self.assertEqual(str(p), '\\\\a\\b\\c') p = self.cls('//a/b/c/d') self.assertEqual(str(p), '\\\\a\\b\\c\\d') def test_str_subclass(self): self._check_str_subclass('c:') self._check_str_subclass('c:a') self._check_str_subclass('c:a\\b.txt') self._check_str_subclass('c:\\') self._check_str_subclass('c:\\a') self._check_str_subclass('c:\\a\\b.txt') self._check_str_subclass('\\\\some\\share') self._check_str_subclass('\\\\some\\share\\a') self._check_str_subclass('\\\\some\\share\\a\\b.txt') def test_eq(self): P = self.cls self.assertEqual(P('c:a/b'), P('c:a/b')) self.assertEqual(P('c:a/b'), P('c:', 'a', 'b')) self.assertNotEqual(P('c:a/b'), P('d:a/b')) self.assertNotEqual(P('c:a/b'), P('c:/a/b')) self.assertNotEqual(P('/a/b'), P('c:/a/b')) # Case-insensitivity. self.assertEqual(P('a/B'), P('A/b')) self.assertEqual(P('C:a/B'), P('c:A/b')) self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b')) def test_as_uri(self): P = self.cls with self.assertRaises(ValueError): P('/a/b').as_uri() with self.assertRaises(ValueError): P('c:a/b').as_uri() self.assertEqual(P('c:/').as_uri(), 'file:///c:/') self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c') self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c') self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9') self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/') self.assertEqual(P('//some/share/a/b.c').as_uri(), 'file://some/share/a/b.c') self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(), 'file://some/share/a/b%25%23c%C3%A9') def test_match_common(self): P = self.cls # Absolute patterns. self.assertTrue(P('c:/b.py').match('/*.py')) self.assertTrue(P('c:/b.py').match('c:*.py')) self.assertTrue(P('c:/b.py').match('c:/*.py')) self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('b.py').match('c:*.py')) self.assertFalse(P('b.py').match('c:/*.py')) self.assertFalse(P('c:b.py').match('/*.py')) self.assertFalse(P('c:b.py').match('c:/*.py')) self.assertFalse(P('/b.py').match('c:*.py')) self.assertFalse(P('/b.py').match('c:/*.py')) # UNC patterns. self.assertTrue(P('//some/share/a.py').match('/*.py')) self.assertTrue(P('//some/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//other/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py')) # Case-insensitivity. self.assertTrue(P('B.py').match('b.PY')) self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY')) self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY')) def test_ordering_common(self): # Case-insensitivity. def assertOrderedEqual(a, b): self.assertLessEqual(a, b) self.assertGreaterEqual(b, a) P = self.cls p = P('c:A/b') q = P('C:a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) p = P('//some/Share/A/b') q = P('//Some/SHARE/a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) def test_parts(self): P = self.cls p = P('c:a/b') parts = p.parts self.assertEqual(parts, ('c:', 'a', 'b')) p = P('c:/a/b') parts = p.parts self.assertEqual(parts, ('c:\\', 'a', 'b')) p = P('//a/b/c/d') parts = p.parts self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd')) def test_parent(self): # Anchored P = self.cls p = P('z:a/b/c') self.assertEqual(p.parent, P('z:a/b')) self.assertEqual(p.parent.parent, P('z:a')) self.assertEqual(p.parent.parent.parent, P('z:')) self.assertEqual(p.parent.parent.parent.parent, P('z:')) p = P('z:/a/b/c') self.assertEqual(p.parent, P('z:/a/b')) self.assertEqual(p.parent.parent, P('z:/a')) self.assertEqual(p.parent.parent.parent, P('z:/')) self.assertEqual(p.parent.parent.parent.parent, P('z:/')) p = P('//a/b/c/d') self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) def test_parents(self): # Anchored P = self.cls p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:a')) self.assertEqual(par[1], P('z:')) self.assertEqual(par[0:1], (P('z:a'),)) self.assertEqual(par[:-1], (P('z:a'),)) self.assertEqual(par[:2], (P('z:a'), P('z:'))) self.assertEqual(par[1:], (P('z:'),)) self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:/a')) self.assertEqual(par[1], P('z:/')) self.assertEqual(par[0:1], (P('z:/a'),)) self.assertEqual(par[0:-1], (P('z:/a'),)) self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) self.assertEqual(par[1:], (P('z:/'),)) self.assertEqual(par[::2], (P('z:/a'),)) self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('//a/b/c/d') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('//a/b/c')) self.assertEqual(par[1], P('//a/b')) self.assertEqual(par[0:1], (P('//a/b/c'),)) self.assertEqual(par[0:-1], (P('//a/b/c'),)) self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b'))) self.assertEqual(par[1:], (P('//a/b'),)) self.assertEqual(par[::2], (P('//a/b/c'),)) self.assertEqual(par[::-1], (P('//a/b'), P('//a/b/c'))) self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')]) with self.assertRaises(IndexError): par[2] def test_drive(self): P = self.cls self.assertEqual(P('c:').drive, 'c:') self.assertEqual(P('c:a/b').drive, 'c:') self.assertEqual(P('c:/').drive, 'c:') self.assertEqual(P('c:/a/b/').drive, 'c:') self.assertEqual(P('//a/b').drive, '\\\\a\\b') self.assertEqual(P('//a/b/').drive, '\\\\a\\b') self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b') def test_root(self): P = self.cls self.assertEqual(P('c:').root, '') self.assertEqual(P('c:a/b').root, '') self.assertEqual(P('c:/').root, '\\') self.assertEqual(P('c:/a/b/').root, '\\') self.assertEqual(P('//a/b').root, '\\') self.assertEqual(P('//a/b/').root, '\\') self.assertEqual(P('//a/b/c/d').root, '\\') def test_anchor(self): P = self.cls self.assertEqual(P('c:').anchor, 'c:') self.assertEqual(P('c:a/b').anchor, 'c:') self.assertEqual(P('c:/').anchor, 'c:\\') self.assertEqual(P('c:/a/b/').anchor, 'c:\\') self.assertEqual(P('//a/b').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/c/d').anchor, '\\\\a\\b\\') def test_name(self): P = self.cls self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') self.assertEqual(P('c:/a/.hgrc').suffix, '') self.assertEqual(P('c:a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) self.assertEqual(P('c:/a/.hgrc').suffixes, []) self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('//My.py/Share.php').suffixes, []) self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) def test_stem(self): P = self.cls self.assertEqual(P('c:').stem, '') self.assertEqual(P('c:.').stem, '') self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name(self): P = self.cls self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_name, '//My/Share') def test_with_stem(self): P = self.cls self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share') def test_with_suffix(self): P = self.cls self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d') def test_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:foO')), P('Bar')) self.assertEqual(p.relative_to('c:foO'), P('Bar')) self.assertEqual(p.relative_to('c:foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:foO/baR')), P()) self.assertEqual(p.relative_to('c:foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('Foo')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) p = P('C:/Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('/Foo/Bar')) self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar') self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar') self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:/foO')), P('Bar')) self.assertEqual(p.relative_to('c:/foO'), P('Bar')) self.assertEqual(p.relative_to('c:/foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:/foO/baR')), P()) self.assertEqual(p.relative_to('c:/foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo')) self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('d:/')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('//C/Foo')) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare'), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo')), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo'), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo')) def test_is_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertTrue(p.is_relative_to(P('c:'))) self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:foO'))) self.assertTrue(p.is_relative_to('c:foO')) self.assertTrue(p.is_relative_to('c:foO/')) self.assertTrue(p.is_relative_to(P('c:foO/baR'))) self.assertTrue(p.is_relative_to('c:foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('Foo'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('C:/Foo'))) self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo/Baz'))) p = P('C:/Foo/Bar') self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:/'))) self.assertTrue(p.is_relative_to(P('c:/foO'))) self.assertTrue(p.is_relative_to('c:/foO/')) self.assertTrue(p.is_relative_to(P('c:/foO/baR'))) self.assertTrue(p.is_relative_to('c:/foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('C:/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo'))) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('d:/'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('//C/Foo'))) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertTrue(p.is_relative_to(P('//sErver/sHare'))) self.assertTrue(p.is_relative_to('//sErver/sHare')) self.assertTrue(p.is_relative_to('//sErver/sHare/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo')) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo/Bar'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/Bar')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('c:/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//z/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//Server/z/Foo'))) def test_is_absolute(self): P = self.cls # Under NT, only paths with both a drive and a root are absolute. self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertFalse(P('/').is_absolute()) self.assertFalse(P('/a').is_absolute()) self.assertFalse(P('/a/b/').is_absolute()) self.assertFalse(P('c:').is_absolute()) self.assertFalse(P('c:a').is_absolute()) self.assertFalse(P('c:a/b/').is_absolute()) self.assertTrue(P('c:/').is_absolute()) self.assertTrue(P('c:/a').is_absolute()) self.assertTrue(P('c:/a/b/').is_absolute()) # UNC paths are absolute by definition. self.assertTrue(P('//a/b').is_absolute()) self.assertTrue(P('//a/b/').is_absolute()) self.assertTrue(P('//a/b/c').is_absolute()) self.assertTrue(P('//a/b/c/d').is_absolute()) def test_join(self): P = self.cls p = P('C:/a/b') pp = p.joinpath('x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. pp = p.joinpath('D:x/y') self.assertEqual(pp, P('D:x/y')) pp = p.joinpath('D:/x/y') self.assertEqual(pp, P('D:/x/y')) pp = p.joinpath('//host/share/x/y') self.assertEqual(pp, P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. pp = p.joinpath('c:x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('c:/x/y') self.assertEqual(pp, P('C:/x/y')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('C:/a/b') self.assertEqual(p / 'x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'x' / 'y', P('C:/a/b/x/y')) self.assertEqual(p / '/x/y', P('C:/x/y')) self.assertEqual(p / '/x' / 'y', P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. self.assertEqual(p / 'D:x/y', P('D:x/y')) self.assertEqual(p / 'D:' / 'x/y', P('D:x/y')) self.assertEqual(p / 'D:/x/y', P('D:/x/y')) self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y')) self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'c:/x/y', P('C:/x/y')) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) # UNC paths are never reserved. self.assertIs(False, P('//my/share/nul/con/aux').is_reserved()) # Case-insensitive DOS-device names are reserved. self.assertIs(True, P('nul').is_reserved()) self.assertIs(True, P('aux').is_reserved()) self.assertIs(True, P('prn').is_reserved()) self.assertIs(True, P('con').is_reserved()) self.assertIs(True, P('conin$').is_reserved()) self.assertIs(True, P('conout$').is_reserved()) # COM/LPT + 1-9 or + superscript 1-3 are reserved. self.assertIs(True, P('COM1').is_reserved()) self.assertIs(True, P('LPT9').is_reserved()) self.assertIs(True, P('com\xb9').is_reserved()) self.assertIs(True, P('com\xb2').is_reserved()) self.assertIs(True, P('lpt\xb3').is_reserved()) # DOS-device name mataching ignores characters after a dot or # a colon and also ignores trailing spaces. self.assertIs(True, P('NUL.txt').is_reserved()) self.assertIs(True, P('PRN ').is_reserved()) self.assertIs(True, P('AUX .txt').is_reserved()) self.assertIs(True, P('COM1:bar').is_reserved()) self.assertIs(True, P('LPT9 :bar').is_reserved()) # DOS-device names are only matched at the beginning # of a path component. self.assertIs(False, P('bar.com9').is_reserved()) self.assertIs(False, P('bar.lpt9').is_reserved()) # Only the last path component matters. self.assertIs(True, P('c:/baz/con/NUL').is_reserved()) self.assertIs(False, P('c:/NUL/con/baz').is_reserved()) class PurePathTest(_BasePurePathTest): cls = pathlib.PurePath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath) def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') self.assertNotEqual(p, q) def test_different_flavours_unordered(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') with self.assertRaises(TypeError): p < q with self.assertRaises(TypeError): p <= q with self.assertRaises(TypeError): p > q with self.assertRaises(TypeError): p >= q # # Tests for the concrete classes. # # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) rel_join = lambda *x: os.path.join(TESTFN, *x) only_nt = unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system') only_posix = unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system') @only_posix class PosixPathAsPureTest(PurePosixPathTest, unittest.TestCase): cls = PosixUPath @only_nt class WindowsPathAsPureTest(PureWindowsPathTest, unittest.TestCase): cls = WindowsUPath def test_owner(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').owner() def test_group(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').group() class _BasePathTest(object): """Tests for the FS-accessing functionalities of the Path classes.""" # (BASE) # | # |-- brokenLink -> non-existing # |-- dirA # | `-- linkC -> ../dirB # |-- dirB # | |-- fileB # | `-- linkD -> ../dirB # |-- dirC # | |-- dirD # | | `-- fileD # | `-- fileC # |-- dirE # No permissions # |-- fileA # |-- linkA -> fileA # |-- linkB -> dirB # `-- brokenLinkLoop -> brokenLinkLoop # def setUp(self): def cleanup(): os.chmod(join('dirE'), 0o777) os_helper.rmtree(BASE) self.addCleanup(cleanup) os.mkdir(BASE) os.mkdir(join('dirA')) os.mkdir(join('dirB')) os.mkdir(join('dirC')) os.mkdir(join('dirC', 'dirD')) os.mkdir(join('dirE')) with open(join('fileA'), 'wb') as f: f.write(b"this is file A\n") with open(join('dirB', 'fileB'), 'wb') as f: f.write(b"this is file B\n") with open(join('dirC', 'fileC'), 'wb') as f: f.write(b"this is file C\n") with open(join('dirC', 'dirD', 'fileD'), 'wb') as f: f.write(b"this is file D\n") os.chmod(join('dirE'), 0) if os_helper.can_symlink(): # Relative symlinks. os.symlink('fileA', join('linkA')) os.symlink('non-existing', join('brokenLink')) self.dirlink('dirB', join('linkB')) self.dirlink(os.path.join('..', 'dirB'), join('dirA', 'linkC')) # This one goes upwards, creating a loop. self.dirlink(os.path.join('..', 'dirB'), join('dirB', 'linkD')) # Broken symlink (pointing to itself). os.symlink('brokenLinkLoop', join('brokenLinkLoop')) if os.name == 'nt': # Workaround for http://bugs.python.org/issue13772. def dirlink(self, src, dest): os.symlink(src, dest, target_is_directory=True) else: def dirlink(self, src, dest): os.symlink(src, dest) def assertSame(self, path_a, path_b): self.assertTrue(os.path.samefile(str(path_a), str(path_b)), "%r and %r don't point to the same file" % (path_a, path_b)) def assertFileNotFound(self, func, *args, **kwargs): with self.assertRaises(FileNotFoundError) as cm: func(*args, **kwargs) self.assertEqual(cm.exception.errno, errno.ENOENT) def assertEqualNormCase(self, path_a, path_b): self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b)) def _test_cwd(self, p): q = self.cls(os.getcwd()) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_cwd(self): p = self.cls.cwd() self._test_cwd(p) def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_home(self): with os_helper.EnvironmentVarGuard() as env: self._test_home(self.cls.home()) env.clear() env['USERPROFILE'] = os.path.join(BASE, 'userprofile') self._test_home(self.cls.home()) # bpo-38883: ignore `HOME` when set on windows env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) def test_samefile(self): fileA_path = os.path.join(BASE, 'fileA') fileB_path = os.path.join(BASE, 'dirB', 'fileB') p = self.cls(fileA_path) pp = self.cls(fileA_path) q = self.cls(fileB_path) self.assertTrue(p.samefile(fileA_path)) self.assertTrue(p.samefile(pp)) self.assertFalse(p.samefile(fileB_path)) self.assertFalse(p.samefile(q)) # Test the non-existent file case non_existent = os.path.join(BASE, 'foo') r = self.cls(non_existent) self.assertRaises(FileNotFoundError, p.samefile, r) self.assertRaises(FileNotFoundError, p.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, p) self.assertRaises(FileNotFoundError, r.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, r) self.assertRaises(FileNotFoundError, r.samefile, non_existent) def test_empty_path(self): # The empty path points to '.' p = self.cls('') self.assertEqual(p.stat(), os.stat('.')) def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') self.assertEqual(p.expanduser(), p) p = P('../~') self.assertEqual(p.expanduser(), p) p = P(P('').absolute().anchor) / '~' self.assertEqual(p.expanduser(), p) def test_exists(self): P = self.cls p = P(BASE) self.assertIs(True, p.exists()) self.assertIs(True, (p / 'dirA').exists()) self.assertIs(True, (p / 'fileA').exists()) self.assertIs(False, (p / 'fileA' / 'bah').exists()) if os_helper.can_symlink(): self.assertIs(True, (p / 'linkA').exists()) self.assertIs(True, (p / 'linkB').exists()) self.assertIs(True, (p / 'linkB' / 'fileB').exists()) self.assertIs(False, (p / 'linkA' / 'bah').exists()) self.assertIs(False, (p / 'foo').exists()) self.assertIs(False, P('/xyzzy').exists()) self.assertIs(False, P(BASE + '\udfff').exists()) self.assertIs(False, P(BASE + '\x00').exists()) def test_open_common(self): p = self.cls(BASE) with (p / 'fileA').open('r') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), "this is file A\n") with (p / 'fileA').open('rb') as f: self.assertIsInstance(f, io.BufferedIOBase) self.assertEqual(f.read().strip(), b"this is file A") with (p / 'fileA').open('rb', buffering=0) as f: self.assertIsInstance(f, io.RawIOBase) self.assertEqual(f.read().strip(), b"this is file A") def test_read_write_bytes(self): p = self.cls(BASE) (p / 'fileA').write_bytes(b'abcdefg') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') # Check that trying to write str does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') def test_read_write_text(self): p = self.cls(BASE) (p / 'fileA').write_text('äbcdefg', encoding='latin-1') self.assertEqual((p / 'fileA').read_text( encoding='utf-8', errors='ignore'), 'bcdefg') # Check that trying to write bytes does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes') self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg') def test_write_text_with_newlines(self): p = self.cls(BASE) # Check that `\n` character change nothing (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\nfghlk\n\rmnopq') # Check that `\r` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\rfghlk\r\rmnopq') # Check that `\r\n` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\r\nfghlk\r\n\rmnopq') # Check that no argument passed will change `\n` to `os.linesep` os_linesep_byte = bytes(os.linesep, encoding='ascii') (p / 'fileA').write_text('abcde\nfghlk\n\rmnopq') self.assertEqual((p / 'fileA').read_bytes(), b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq') def test_iterdir(self): P = self.cls p = P(BASE) it = p.iterdir() paths = set(it) expected = ['dirA', 'dirB', 'dirC', 'dirE', 'fileA'] if os_helper.can_symlink(): expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop'] self.assertEqual(paths, { P(BASE, q) for q in expected }) @os_helper.skip_unless_symlink def test_iterdir_symlink(self): # __iter__ on a symlink to a directory. P = self.cls p = P(BASE, 'linkB') paths = set(p.iterdir()) expected = { P(BASE, 'linkB', q) for q in ['fileB', 'linkD'] } self.assertEqual(paths, expected) def test_iterdir_nodir(self): # __iter__ on something that is not a directory. p = self.cls(BASE, 'fileA') with self.assertRaises(OSError) as cm: next(p.iterdir()) # ENOENT or EINVAL under Windows, ENOTDIR otherwise # (see issue #12802). self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) def test_glob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.glob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.glob("fileB"), []) _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) else: _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) if not os_helper.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not os_helper.can_symlink(): _check(p.glob("*/fileB"), ['dirB/fileB']) else: _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) def test_rglob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.rglob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) if not os_helper.can_symlink(): _check(p.rglob("*/fileB"), ["dirB/fileB"]) else: _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) p = P(BASE, "dirC") _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) @os_helper.skip_unless_symlink def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) given = set(p.rglob('*')) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', 'dirC', 'dirC/dirD', 'dirC/dirD/fileD', 'dirC/fileC', 'dirE', 'fileA', 'linkA', 'linkB', 'brokenLinkLoop', } self.assertEqual(given, {p / x for x in expect}) def test_glob_many_open_files(self): depth = 30 P = self.cls base = P(BASE) / 'deep' p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) iters = [base.glob(pattern) for j in range(100)] for it in iters: self.assertEqual(next(it), p) iters = [base.rglob('d') for j in range(100)] p = base for i in range(depth): p = p / 'd' for it in iters: self.assertEqual(next(it), p) def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) self.assertEqual(set(p.glob("../xyzzy")), set()) @os_helper.skip_unless_symlink def test_glob_permissions(self): # See bpo-38894 P = self.cls base = P(BASE) / 'permissions' base.mkdir() file1 = base / "file1" file1.touch() file2 = base / "file2" file2.touch() subdir = base / "subdir" file3 = base / "file3" file3.symlink_to(subdir / "other") # Patching is needed to avoid relying on the filesystem # to return the order of the files as the error will not # happen if the symlink is the last item. with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 3) subdir.mkdir() with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 4) subdir.chmod(000) with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 4) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) self.assertEqual(q, expected) # This can be used to check both relative and absolute resolutions. _check_resolve_relative = _check_resolve_absolute = _check_resolve @os_helper.skip_unless_symlink def test_resolve_common(self): P = self.cls p = P(BASE, 'foo') with self.assertRaises(OSError) as cm: p.resolve(strict=True) self.assertEqual(cm.exception.errno, errno.ENOENT) # Non-strict self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo')) p = P(BASE, 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo', 'in', 'spam')) p = P(BASE, '..', 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.abspath(os.path.join('foo', 'in', 'spam'))) # These are all relative symlinks. p = P(BASE, 'dirB', 'fileB') self._check_resolve_relative(p, p) p = P(BASE, 'linkA') self._check_resolve_relative(p, P(BASE, 'fileA')) p = P(BASE, 'dirA', 'linkC', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) p = P(BASE, 'dirB', 'linkD', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(BASE, 'dirA', 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) # Now create absolute symlinks. d = os_helper._longpath(tempfile.mkdtemp(suffix='-dirD', dir=os.getcwd())) self.addCleanup(os_helper.rmtree, d) os.symlink(os.path.join(d), join('dirA', 'linkX')) os.symlink(join('dirB'), os.path.join(d, 'linkY')) p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB') self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) @os_helper.skip_unless_symlink def test_resolve_dot(self): # See https://bitbucket.org/pitrou/pathlib/issue/9/pathresolve-fails-on-complex-symlinks p = self.cls(BASE) self.dirlink('.', join('0')) self.dirlink(os.path.join('0', '0'), join('1')) self.dirlink(os.path.join('1', '1'), join('2')) q = p / '2' self.assertEqual(q.resolve(strict=True), p) r = q / '3' / '4' self.assertRaises(FileNotFoundError, r.resolve, strict=True) # Non-strict self.assertEqual(r.resolve(strict=False), p / '3' / '4') def test_resolve_nonexist_relative_issue38671(self): p = self.cls('non', 'exist') old_cwd = os.getcwd() os.chdir(BASE) try: self.assertEqual(p.resolve(), self.cls(BASE, p)) finally: os.chdir(old_cwd) def test_with(self): p = self.cls(BASE) it = p.iterdir() it2 = p.iterdir() next(it2) with p: pass # Using a path as a context manager is a no-op, thus the following # operations should still succeed after the context manage exits. next(it) next(it2) p.exists() p.resolve() p.absolute() with p: pass def test_chmod(self): p = self.cls(BASE) / 'fileA' mode = p.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # Set writable bit. new_mode = mode | 0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # On Windows, os.chmod does not follow symlinks (issue #15411) @only_posix def test_chmod_follow_symlinks_true(self): p = self.cls(BASE) / 'linkA' q = p.resolve() mode = q.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # Set writable bit new_mode = mode | 0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # XXX also need a test for lchmod. def test_stat(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(p.stat(), st) # Change file mode by flipping write bit. p.chmod(st.st_mode ^ 0o222) self.addCleanup(p.chmod, st.st_mode) self.assertNotEqual(p.stat(), st) @os_helper.skip_unless_symlink def test_stat_no_follow_symlinks(self): p = self.cls(BASE) / 'linkA' st = p.stat() self.assertNotEqual(st, p.stat(follow_symlinks=False)) def test_stat_no_follow_symlinks_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.stat(follow_symlinks=False)) @os_helper.skip_unless_symlink def test_lstat(self): p = self.cls(BASE)/ 'linkA' st = p.stat() self.assertNotEqual(st, p.lstat()) def test_lstat_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.lstat()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") def test_owner(self): p = self.cls(BASE) / 'fileA' uid = p.stat().st_uid try: name = pwd.getpwuid(uid).pw_name except KeyError: self.skipTest( "user %d doesn't have an entry in the system database" % uid) self.assertEqual(name, p.owner()) @unittest.skipUnless(grp, "the grp module is needed for this test") def test_group(self): p = self.cls(BASE) / 'fileA' gid = p.stat().st_gid try: name = grp.getgrgid(gid).gr_name except KeyError: self.skipTest( "group %d doesn't have an entry in the system database" % gid) self.assertEqual(name, p.group()) def test_unlink(self): p = self.cls(BASE) / 'fileA' p.unlink() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) def test_unlink_missing_ok(self): p = self.cls(BASE) / 'fileAAA' self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self): p = self.cls(BASE) / 'dirA' for q in p.iterdir(): q.unlink() p.rmdir() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_link_to(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # linking to another path. q = P / 'dirA' / 'fileAA' try: with self.assertWarns(DeprecationWarning): p.link_to(q) except PermissionError as e: self.skipTest('os.link(): %s' % e) self.assertEqual(q.stat().st_size, size) self.assertEqual(os.path.samefile(p, q), True) self.assertTrue(p.stat) # Linking to a str of a relative path. r = rel_join('fileAAA') with self.assertWarns(DeprecationWarning): q.link_to(r) self.assertEqual(os.stat(r).st_size, size) self.assertTrue(q.stat) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_hardlink_to(self): P = self.cls(BASE) target = P / 'fileA' size = target.stat().st_size # linking to another path. link = P / 'dirA' / 'fileAA' link.hardlink_to(target) self.assertEqual(link.stat().st_size, size) self.assertTrue(os.path.samefile(target, link)) self.assertTrue(target.exists()) # Linking to a str of a relative path. link2 = P / 'dirA' / 'fileAAA' target2 = rel_join('fileA') link2.hardlink_to(target2) self.assertEqual(os.stat(target2).st_size, size) self.assertTrue(link2.exists()) @unittest.skipIf(hasattr(os, "link"), "os.link() is present") def test_link_to_not_implemented(self): P = self.cls(BASE) p = P / 'fileA' # linking to another path. q = P / 'dirA' / 'fileAA' with self.assertRaises(NotImplementedError): p.link_to(q) def test_rename(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Renaming to another path. q = P / 'dirA' / 'fileAA' renamed_p = p.rename(q) self.assertEqual(renamed_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Renaming to a str of a relative path. r = rel_join('fileAAA') renamed_q = q.rename(r) self.assertEqual(renamed_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) def test_replace(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Replacing a non-existing path. q = P / 'dirA' / 'fileAA' replaced_p = p.replace(q) self.assertEqual(replaced_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Replacing another (existing) path. r = rel_join('dirB', 'fileB') replaced_q = q.replace(r) self.assertEqual(replaced_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) @os_helper.skip_unless_symlink def test_readlink(self): P = self.cls(BASE) self.assertEqual((P / 'linkA').readlink(), self.cls('fileA')) self.assertEqual((P / 'brokenLink').readlink(), self.cls('non-existing')) self.assertEqual((P / 'linkB').readlink(), self.cls('dirB')) with self.assertRaises(OSError): (P / 'fileA').readlink() def test_touch_common(self): P = self.cls(BASE) p = P / 'newfileA' self.assertFalse(p.exists()) p.touch() self.assertTrue(p.exists()) st = p.stat() old_mtime = st.st_mtime old_mtime_ns = st.st_mtime_ns # Rewind the mtime sufficiently far in the past to work around # filesystem-specific timestamp granularity. os.utime(str(p), (old_mtime - 10, old_mtime - 10)) # The file mtime should be refreshed by calling touch() again. p.touch() st = p.stat() self.assertGreaterEqual(st.st_mtime_ns, old_mtime_ns) self.assertGreaterEqual(st.st_mtime, old_mtime) # Now with exist_ok=False. p = P / 'newfileB' self.assertFalse(p.exists()) p.touch(mode=0o700, exist_ok=False) self.assertTrue(p.exists()) self.assertRaises(OSError, p.touch, exist_ok=False) def test_touch_nochange(self): P = self.cls(BASE) p = P / 'fileA' p.touch() with p.open('rb') as f: self.assertEqual(f.read().strip(), b"this is file A") def test_mkdir(self): P = self.cls(BASE) p = P / 'newdirA' self.assertFalse(p.exists()) p.mkdir() self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_parents(self): # Creating a chain of directories. p = self.cls(BASE, 'newdirB', 'newdirC') self.assertFalse(p.exists()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.ENOENT) p.mkdir(parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) # Test `mode` arg. mode = stat.S_IMODE(p.stat().st_mode) # Default mode. p = self.cls(BASE, 'newdirD', 'newdirE') p.mkdir(0o555, parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) if os.name != 'nt': # The directory's permissions follow the mode argument. self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o7555 & mode) # The parent's permissions follow the default process settings. self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode) def test_mkdir_exist_ok(self): p = self.cls(BASE, 'dirB') st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_with_parent(self): p = self.cls(BASE, 'dirC') self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p = p / 'newdirC' p.mkdir(parents=True) st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(parents=True, exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_root(self): # Issue #25803: A drive root could raise PermissionError on Windows. self.cls('/').resolve().mkdir(exist_ok=True) self.cls('/').resolve().mkdir(parents=True, exist_ok=True) @only_nt # XXX: not sure how to test this on POSIX. def test_mkdir_with_unknown_drive(self): for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA': p = self.cls(d + ':\\') if not p.is_dir(): break else: self.skipTest("cannot find a drive that doesn't exist") with self.assertRaises(OSError): (p / 'child' / 'path').mkdir(parents=True) def test_mkdir_with_child_file(self): p = self.cls(BASE, 'dirB', 'fileB') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True, exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_no_parents_file(self): p = self.cls(BASE, 'fileA') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_concurrent_parent_creation(self): for pattern_num in range(32): p = self.cls(BASE, 'dirCPC%d' % pattern_num) self.assertFalse(p.exists()) def my_mkdir(path, mode=0o777): path = str(path) # Emulate another process that would create the directory # just before we try to create it ourselves. We do it # in all possible pattern combinations, assuming that this # function is called at most 5 times (dirCPC/dir1/dir2, # dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2). if pattern.pop(): os.mkdir(path, mode) # From another process. concurrently_created.add(path) os.mkdir(path, mode) # Our real call. pattern = [bool(pattern_num & (1 << n)) for n in range(5)] concurrently_created = set() p12 = p / 'dir1' / 'dir2' try: with mock.patch("pathlib._normal_accessor.mkdir", my_mkdir): p12.mkdir(parents=True, exist_ok=False) except FileExistsError: self.assertIn(str(p12), concurrently_created) else: self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) @os_helper.skip_unless_symlink def test_symlink_to(self): P = self.cls(BASE) target = P / 'fileA' # Symlinking a path target. link = P / 'dirA' / 'linkAA' link.symlink_to(target) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) # Symlinking a str target. link = P / 'dirA' / 'linkAAA' link.symlink_to(str(target)) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertFalse(link.is_dir()) # Symlinking to a directory. target = P / 'dirB' link = P / 'dirA' / 'linkAAAA' link.symlink_to(target, target_is_directory=True) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertTrue(link.is_dir()) self.assertTrue(list(link.iterdir())) def test_is_dir(self): P = self.cls(BASE) self.assertTrue((P / 'dirA').is_dir()) self.assertFalse((P / 'fileA').is_dir()) self.assertFalse((P / 'non-existing').is_dir()) self.assertFalse((P / 'fileA' / 'bah').is_dir()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_dir()) self.assertTrue((P / 'linkB').is_dir()) self.assertFalse((P/ 'brokenLink').is_dir(), False) self.assertIs((P / 'dirA\udfff').is_dir(), False) self.assertIs((P / 'dirA\x00').is_dir(), False) def test_is_file(self): P = self.cls(BASE) self.assertTrue((P / 'fileA').is_file()) self.assertFalse((P / 'dirA').is_file()) self.assertFalse((P / 'non-existing').is_file()) self.assertFalse((P / 'fileA' / 'bah').is_file()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_file()) self.assertFalse((P / 'linkB').is_file()) self.assertFalse((P/ 'brokenLink').is_file()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) @only_posix def test_is_mount(self): P = self.cls(BASE) R = self.cls('/') # TODO: Work out Windows. self.assertFalse((P / 'fileA').is_mount()) self.assertFalse((P / 'dirA').is_mount()) self.assertFalse((P / 'non-existing').is_mount()) self.assertFalse((P / 'fileA' / 'bah').is_mount()) self.assertTrue(R.is_mount()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_mount()) self.assertIs(self.cls('/\udfff').is_mount(), False) self.assertIs(self.cls('/\x00').is_mount(), False) def test_is_symlink(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_symlink()) self.assertFalse((P / 'dirA').is_symlink()) self.assertFalse((P / 'non-existing').is_symlink()) self.assertFalse((P / 'fileA' / 'bah').is_symlink()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_symlink()) self.assertTrue((P / 'linkB').is_symlink()) self.assertTrue((P/ 'brokenLink').is_symlink()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) if os_helper.can_symlink(): self.assertIs((P / 'linkA\udfff').is_file(), False) self.assertIs((P / 'linkA\x00').is_file(), False) def test_is_fifo_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_fifo()) self.assertFalse((P / 'dirA').is_fifo()) self.assertFalse((P / 'non-existing').is_fifo()) self.assertFalse((P / 'fileA' / 'bah').is_fifo()) self.assertIs((P / 'fileA\udfff').is_fifo(), False) self.assertIs((P / 'fileA\x00').is_fifo(), False) @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required") @unittest.skipIf(sys.platform == "vxworks", "fifo requires special path on VxWorks") def test_is_fifo_true(self): P = self.cls(BASE, 'myfifo') try: os.mkfifo(str(P)) except PermissionError as e: self.skipTest('os.mkfifo(): %s' % e) self.assertTrue(P.is_fifo()) self.assertFalse(P.is_socket()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'myfifo\udfff').is_fifo(), False) self.assertIs(self.cls(BASE, 'myfifo\x00').is_fifo(), False) def test_is_socket_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_socket()) self.assertFalse((P / 'dirA').is_socket()) self.assertFalse((P / 'non-existing').is_socket()) self.assertFalse((P / 'fileA' / 'bah').is_socket()) self.assertIs((P / 'fileA\udfff').is_socket(), False) self.assertIs((P / 'fileA\x00').is_socket(), False) @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") def test_is_socket_true(self): P = self.cls(BASE, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.addCleanup(sock.close) try: sock.bind(str(P)) except OSError as e: if (isinstance(e, PermissionError) or "AF_UNIX path too long" in str(e)): self.skipTest("cannot bind Unix socket: " + str(e)) self.assertTrue(P.is_socket()) self.assertFalse(P.is_fifo()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'mysock\udfff').is_socket(), False) self.assertIs(self.cls(BASE, 'mysock\x00').is_socket(), False) def test_is_block_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_block_device()) self.assertFalse((P / 'dirA').is_block_device()) self.assertFalse((P / 'non-existing').is_block_device()) self.assertFalse((P / 'fileA' / 'bah').is_block_device()) self.assertIs((P / 'fileA\udfff').is_block_device(), False) self.assertIs((P / 'fileA\x00').is_block_device(), False) def test_is_char_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_char_device()) self.assertFalse((P / 'dirA').is_char_device()) self.assertFalse((P / 'non-existing').is_char_device()) self.assertFalse((P / 'fileA' / 'bah').is_char_device()) self.assertIs((P / 'fileA\udfff').is_char_device(), False) self.assertIs((P / 'fileA\x00').is_char_device(), False) def test_is_char_device_true(self): # Under Unix, /dev/null should generally be a char device. P = self.cls('/dev/null') if not P.exists(): self.skipTest("/dev/null required") self.assertTrue(P.is_char_device()) self.assertFalse(P.is_block_device()) self.assertFalse(P.is_file()) self.assertIs(self.cls('/dev/null\udfff').is_char_device(), False) self.assertIs(self.cls('/dev/null\x00').is_char_device(), False) def test_pickling_common(self): p = self.cls(BASE, 'fileA') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertEqual(pp.stat(), p.stat()) def test_parts_interning(self): P = self.cls p = P('/usr/bin/foo') q = P('/usr/local/bin') # 'usr' self.assertIs(p.parts[1], q.parts[1]) # 'bin' self.assertIs(p.parts[2], q.parts[3]) def _check_complex_symlinks(self, link0_target): # Test solving a non-looping chain of symlinks (issue #19887). P = self.cls(BASE) self.dirlink(os.path.join('link0', 'link0'), join('link1')) self.dirlink(os.path.join('link1', 'link1'), join('link2')) self.dirlink(os.path.join('link2', 'link2'), join('link3')) self.dirlink(link0_target, join('link0')) # Resolve absolute paths. p = (P / 'link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) # Resolve relative paths. old_path = os.getcwd() os.chdir(BASE) try: p = self.cls('link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) finally: os.chdir(old_path) @os_helper.skip_unless_symlink def test_complex_symlinks_absolute(self): self._check_complex_symlinks(BASE) @os_helper.skip_unless_symlink def test_complex_symlinks_relative(self): self._check_complex_symlinks('.') @os_helper.skip_unless_symlink def test_complex_symlinks_relative_dot_dot(self): self._check_complex_symlinks(os.path.join('dirA', '..')) class PathTest(_BasePathTest, unittest.TestCase): cls = UPath def test_class_getitem(self): self.assertIs(self.cls[str], self.cls) def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), WindowsUPath if os.name == 'nt' else PosixUPath) def test_unsupported_flavour(self): if os.name == 'nt': self.assertRaises(NotImplementedError, PosixUPath) else: self.assertRaises(NotImplementedError, WindowsUPath) def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): list(p.glob('')) @only_posix class PosixPathTest(_BasePathTest, unittest.TestCase): cls = PosixUPath def _check_symlink_loop(self, *args, strict=True): path = self.cls(*args) with self.assertRaises(RuntimeError): print(path.resolve(strict)) def test_open_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) with (p / 'new_file').open('wb'): pass st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) with (p / 'other_new_file').open('wb'): pass st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) def test_resolve_root(self): current_directory = os.getcwd() try: os.chdir('/') p = self.cls('spam') self.assertEqual(str(p.resolve()), '/spam') finally: os.chdir(current_directory) def test_touch_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) (p / 'new_file').touch() st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) (p / 'other_new_file').touch() st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) (p / 'masked_new_file').touch(mode=0o750) st = os.stat(join('masked_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o750) @os_helper.skip_unless_symlink def test_resolve_loop(self): # Loops with relative symlinks. os.symlink('linkX/inside', join('linkX')) self._check_symlink_loop(BASE, 'linkX') os.symlink('linkY', join('linkY')) self._check_symlink_loop(BASE, 'linkY') os.symlink('linkZ/../linkZ', join('linkZ')) self._check_symlink_loop(BASE, 'linkZ') # Non-strict self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False) # Loops with absolute symlinks. os.symlink(join('linkU/inside'), join('linkU')) self._check_symlink_loop(BASE, 'linkU') os.symlink(join('linkV'), join('linkV')) self._check_symlink_loop(BASE, 'linkV') os.symlink(join('linkW/../linkW'), join('linkW')) self._check_symlink_loop(BASE, 'linkW') # Non-strict self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False) def test_glob(self): P = self.cls p = P(BASE) given = set(p.glob("FILEa")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.glob("FILEa*")), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") given = set(p.rglob("FILEd")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.rglob("FILEd*")), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') @unittest.skipIf(sys.platform == "vxworks", "no home directory on VxWorks") def test_expanduser(self): P = self.cls import_helper.import_module('pwd') import pwd pwdent = pwd.getpwuid(os.getuid()) username = pwdent.pw_name userhome = pwdent.pw_dir.rstrip('/') or '/' # Find arbitrary different user (if exists). for pwdent in pwd.getpwall(): othername = pwdent.pw_name otherhome = pwdent.pw_dir.rstrip('/') if othername != username and otherhome: break else: othername = username otherhome = userhome fakename = 'fakeuser' # This user can theoretically exist on a test runner. Create unique name: try: while pwd.getpwnam(fakename): fakename += '1' except KeyError: pass # Non-existent name found p1 = P('~/Documents') p2 = P(f'~{username}/Documents') p3 = P(f'~{othername}/Documents') p4 = P(f'../~{username}/Documents') p5 = P(f'/~{username}/Documents') p6 = P('') p7 = P(f'~{fakename}/Documents') with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) self.assertEqual(p1.expanduser(), P(userhome) / 'Documents') self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) env['HOME'] = '/tmp' self.assertEqual(p1.expanduser(), P('/tmp/Documents')) self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) @unittest.skipIf(sys.platform != "darwin", "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: file_descriptors = list(UPath('/dev/fd').rglob("*"))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee # which one will fail. for f in file_descriptors: f.exists() f.is_dir() f.is_file() f.is_symlink() f.is_block_device() f.is_char_device() f.is_fifo() f.is_socket() except OSError as e: if e.errno == errno.EBADF: self.fail("Bad file descriptor not handled.") raise @only_nt class WindowsPathTest(_BasePathTest, unittest.TestCase): cls = WindowsUPath def test_glob(self): P = self.cls p = P(BASE) self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\FILEa"}) self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\FILEd"}) def test_expanduser(self): P = self.cls with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) env.pop('USERPROFILE', None) env.pop('HOMEPATH', None) env.pop('HOMEDRIVE', None) env['USERNAME'] = 'alice' # test that the path returns unchanged p1 = P('~/My Documents') p2 = P('~alice/My Documents') p3 = P('~bob/My Documents') p4 = P('/~/My Documents') p5 = P('d:~/My Documents') p6 = P('') self.assertRaises(RuntimeError, p1.expanduser) self.assertRaises(RuntimeError, p2.expanduser) self.assertRaises(RuntimeError, p3.expanduser) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) def check(): env.pop('USERNAME', None) self.assertEqual(p1.expanduser(), P('C:/Users/alice/My Documents')) self.assertRaises(RuntimeError, p2.expanduser) env['USERNAME'] = 'alice' self.assertEqual(p2.expanduser(), P('C:/Users/alice/My Documents')) self.assertEqual(p3.expanduser(), P('C:/Users/bob/My Documents')) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) env['HOMEPATH'] = 'C:\\Users\\alice' check() env['HOMEDRIVE'] = 'C:\\' env['HOMEPATH'] = 'Users\\alice' check() env.pop('HOMEDRIVE', None) env.pop('HOMEPATH', None) env['USERPROFILE'] = 'C:\\Users\\alice' check() # bpo-38883: ignore `HOME` when set on windows env['HOME'] = 'C:\\Users\\eve' check() universal_pathlib-0.3.10/upath/tests/pathlib/test_pathlib_311.py000066400000000000000000003254021514661127100246460ustar00rootroot00000000000000import contextlib import collections.abc import io import os import sys import errno import pathlib import pickle import socket import stat import tempfile import unittest from unittest import mock from ._test_support import import_helper from ._test_support import is_emscripten, is_wasi from . import _test_support as os_helper from ._test_support import TESTFN, FakePath try: import grp, pwd except ImportError: grp = pwd = None from upath.core import UPath from upath.implementations.local import PosixUPath, WindowsUPath import pytest pytestmark = pytest.mark.skipif(sys.version_info[:2] != (3, 11), reason="py311 only") # # Tests for the pure classes. # class _BasePurePathTest(object): # Keys are canonical paths, values are list of tuples of arguments # supposed to produce equal paths. equivalences = { 'a/b': [ ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'), ('a/b/',), ('a//b',), ('a//b//',), # Empty components get removed. ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''), ], '/b/c/d': [ ('a', '/b/c', 'd'), ('a', '///b//c', 'd/'), ('/a', '/b/c', 'd'), # Empty components get removed. ('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'), ], } def setUp(self): p = self.cls('a') self.flavour = p._flavour self.sep = self.flavour.sep self.altsep = self.flavour.altsep def test_constructor_common(self): P = self.cls p = P('a') self.assertIsInstance(p, P) P('a', 'b', 'c') P('/a', 'b', 'c') P('a/b/c') P('/a/b/c') P(FakePath("a/b/c")) self.assertEqual(P(P('a')), P('a')) self.assertEqual(P(P('a'), 'b'), P('a/b')) self.assertEqual(P(P('a'), P('b')), P('a/b')) self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c"))) def _check_str_subclass(self, *args): # Issue #21127: it should be possible to construct a PurePath object # from a str subclass instance, and it then gets converted to # a pure str object. class StrSubclass(str): pass P = self.cls p = P(*(StrSubclass(x) for x in args)) self.assertEqual(p, P(*args)) for part in p.parts: self.assertIs(type(part), str) def test_str_subclass_common(self): self._check_str_subclass('') self._check_str_subclass('.') self._check_str_subclass('a') self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') def test_join_common(self): P = self.cls p = P('a/b') pp = p.joinpath('c') self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p.joinpath('c', 'd') self.assertEqual(pp, P('a/b/c/d')) pp = p.joinpath(P('c')) self.assertEqual(pp, P('a/b/c')) pp = p.joinpath('/c') self.assertEqual(pp, P('/c')) def test_div_common(self): # Basically the same as joinpath(). P = self.cls p = P('a/b') pp = p / 'c' self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p / 'c/d' self.assertEqual(pp, P('a/b/c/d')) pp = p / 'c' / 'd' self.assertEqual(pp, P('a/b/c/d')) pp = 'c' / p / 'd' self.assertEqual(pp, P('c/a/b/d')) pp = p / P('c') self.assertEqual(pp, P('a/b/c')) pp = p/ '/c' self.assertEqual(pp, P('/c')) def _check_str(self, expected, args): p = self.cls(*args) self.assertEqual(str(p), expected.replace('/', self.sep)) def test_str_common(self): # Canonicalized paths roundtrip. for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self._check_str(pathstr, (pathstr,)) # Special case for the empty path. self._check_str('.', ('',)) # Other tests for str() are in test_equivalences(). def test_as_posix_common(self): P = self.cls for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self.assertEqual(P(pathstr).as_posix(), pathstr) # Other tests for as_posix() are in test_equivalences(). def test_as_bytes_common(self): sep = os.fsencode(self.sep) P = self.cls self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b') def test_as_uri_common(self): P = self.cls with self.assertRaises(ValueError): P('a').as_uri() with self.assertRaises(ValueError): P().as_uri() def test_repr_common(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): p = self.cls(pathstr) clsname = p.__class__.__name__ r = repr(p) # The repr() is in the form ClassName("forward-slashes path"). self.assertTrue(r.startswith(clsname + '('), r) self.assertTrue(r.endswith(')'), r) inner = r[len(clsname) + 1 : -1] self.assertEqual(eval(inner), p.as_posix()) # The repr() roundtrips. q = eval(r, {"PosixUPath": PosixUPath, "WindowsUPath": WindowsUPath}) self.assertIs(q.__class__, p.__class__) self.assertEqual(q, p) self.assertEqual(repr(q), r) def test_eq_common(self): P = self.cls self.assertEqual(P('a/b'), P('a/b')) self.assertEqual(P('a/b'), P('a', 'b')) self.assertNotEqual(P('a/b'), P('a')) self.assertNotEqual(P('a/b'), P('/a/b')) self.assertNotEqual(P('a/b'), P()) self.assertNotEqual(P('/a/b'), P('/')) self.assertNotEqual(P(), P('/')) self.assertNotEqual(P(), "") self.assertNotEqual(P(), {}) self.assertNotEqual(P(), int) def test_match_common(self): P = self.cls self.assertRaises(ValueError, P('a').match, '') self.assertRaises(ValueError, P('a').match, '.') # Simple relative pattern. self.assertTrue(P('b.py').match('b.py')) self.assertTrue(P('a/b.py').match('b.py')) self.assertTrue(P('/a/b.py').match('b.py')) self.assertFalse(P('a.py').match('b.py')) self.assertFalse(P('b/py').match('b.py')) self.assertFalse(P('/a.py').match('b.py')) self.assertFalse(P('b.py/c').match('b.py')) # Wildcard relative pattern. self.assertTrue(P('b.py').match('*.py')) self.assertTrue(P('a/b.py').match('*.py')) self.assertTrue(P('/a/b.py').match('*.py')) self.assertFalse(P('b.pyc').match('*.py')) self.assertFalse(P('b./py').match('*.py')) self.assertFalse(P('b.py/c').match('*.py')) # Multi-part relative pattern. self.assertTrue(P('ab/c.py').match('a*/*.py')) self.assertTrue(P('/d/ab/c.py').match('a*/*.py')) self.assertFalse(P('a.py').match('a*/*.py')) self.assertFalse(P('/dab/c.py').match('a*/*.py')) self.assertFalse(P('ab/c.py/d').match('a*/*.py')) # Absolute pattern. self.assertTrue(P('/b.py').match('/*.py')) self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('a/b.py').match('/*.py')) self.assertFalse(P('/a/b.py').match('/*.py')) # Multi-part absolute pattern. self.assertTrue(P('/a/b.py').match('/a/*.py')) self.assertFalse(P('/ab.py').match('/a/*.py')) self.assertFalse(P('/a/b/c.py').match('/a/*.py')) # Multi-part glob-style pattern. self.assertFalse(P('/a/b/c.py').match('/**/*.py')) self.assertTrue(P('/a/b/c.py').match('/a/**/*.py')) def test_ordering_common(self): # Ordering is tuple-alike. def assertLess(a, b): self.assertLess(a, b) self.assertGreater(b, a) P = self.cls a = P('a') b = P('a/b') c = P('abc') d = P('b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) P = self.cls a = P('/a') b = P('/a/b') c = P('/abc') d = P('/b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) with self.assertRaises(TypeError): P() < {} def test_parts_common(self): # `parts` returns a tuple. sep = self.sep P = self.cls p = P('a/b') parts = p.parts self.assertEqual(parts, ('a', 'b')) # The object gets reused. self.assertIs(parts, p.parts) # When the path is absolute, the anchor is a separate part. p = P('/a/b') parts = p.parts self.assertEqual(parts, (sep, 'a', 'b')) def test_fspath_common(self): P = self.cls p = P('a/b') self._check_str(p.__fspath__(), ('a/b',)) self._check_str(os.fspath(p), ('a/b',)) def test_equivalences(self): for k, tuples in self.equivalences.items(): canon = k.replace('/', self.sep) posix = k.replace(self.sep, '/') if canon != posix: tuples = tuples + [ tuple(part.replace('/', self.sep) for part in t) for t in tuples ] tuples.append((posix, )) pcanon = self.cls(canon) for t in tuples: p = self.cls(*t) self.assertEqual(p, pcanon, "failed with args {}".format(t)) self.assertEqual(hash(p), hash(pcanon)) self.assertEqual(str(p), canon) self.assertEqual(p.as_posix(), posix) def test_parent_common(self): # Relative P = self.cls p = P('a/b/c') self.assertEqual(p.parent, P('a/b')) self.assertEqual(p.parent.parent, P('a')) self.assertEqual(p.parent.parent.parent, P()) self.assertEqual(p.parent.parent.parent.parent, P()) # Anchored p = P('/a/b/c') self.assertEqual(p.parent, P('/a/b')) self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative P = self.cls p = P('a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('a/b')) self.assertEqual(par[1], P('a')) self.assertEqual(par[2], P('.')) self.assertEqual(par[-1], P('.')) self.assertEqual(par[-2], P('a')) self.assertEqual(par[-3], P('a/b')) self.assertEqual(par[0:1], (P('a/b'),)) self.assertEqual(par[:2], (P('a/b'), P('a'))) self.assertEqual(par[:-1], (P('a/b'), P('a'))) self.assertEqual(par[1:], (P('a'), P('.'))) self.assertEqual(par[::2], (P('a/b'), P('.'))) self.assertEqual(par[::-1], (P('.'), P('a'), P('a/b'))) self.assertEqual(list(par), [P('a/b'), P('a'), P('.')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] with self.assertRaises(TypeError): par[0] = p # Anchored p = P('/a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('/a/b')) self.assertEqual(par[1], P('/a')) self.assertEqual(par[2], P('/')) self.assertEqual(par[-1], P('/')) self.assertEqual(par[-2], P('/a')) self.assertEqual(par[-3], P('/a/b')) self.assertEqual(par[0:1], (P('/a/b'),)) self.assertEqual(par[:2], (P('/a/b'), P('/a'))) self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) self.assertEqual(par[1:], (P('/a'), P('/'))) self.assertEqual(par[::2], (P('/a/b'), P('/'))) self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] def test_drive_common(self): P = self.cls self.assertEqual(P('a/b').drive, '') self.assertEqual(P('/a/b').drive, '') self.assertEqual(P('').drive, '') def test_root_common(self): P = self.cls sep = self.sep self.assertEqual(P('').root, '') self.assertEqual(P('a/b').root, '') self.assertEqual(P('/').root, sep) self.assertEqual(P('/a/b').root, sep) def test_anchor_common(self): P = self.cls sep = self.sep self.assertEqual(P('').anchor, '') self.assertEqual(P('a/b').anchor, '') self.assertEqual(P('/').anchor, sep) self.assertEqual(P('/a/b').anchor, sep) def test_name_common(self): P = self.cls self.assertEqual(P('').name, '') self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') self.assertEqual(P('/a/b').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') def test_suffix_common(self): P = self.cls self.assertEqual(P('').suffix, '') self.assertEqual(P('.').suffix, '') self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') self.assertEqual(P('/a/b').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') self.assertEqual(P('a/.hgrc').suffix, '') self.assertEqual(P('/a/.hgrc').suffix, '') self.assertEqual(P('a/.hg.rc').suffix, '.rc') self.assertEqual(P('/a/.hg.rc').suffix, '.rc') self.assertEqual(P('a/b.tar.gz').suffix, '.gz') self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') def test_suffixes_common(self): P = self.cls self.assertEqual(P('').suffixes, []) self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) self.assertEqual(P('a/.hgrc').suffixes, []) self.assertEqual(P('/a/.hgrc').suffixes, []) self.assertEqual(P('a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) def test_stem_common(self): P = self.cls self.assertEqual(P('').stem, '') self.assertEqual(P('.').stem, '') self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name_common(self): P = self.cls self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') self.assertRaises(ValueError, P('a/b').with_name, '') self.assertRaises(ValueError, P('a/b').with_name, '/c') self.assertRaises(ValueError, P('a/b').with_name, 'c/') self.assertRaises(ValueError, P('a/b').with_name, 'c/d') def test_with_stem_common(self): P = self.cls self.assertEqual(P('a/b').with_stem('d'), P('a/d')) self.assertEqual(P('/a/b').with_stem('d'), P('/a/d')) self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py')) self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py')) self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') self.assertRaises(ValueError, P('a/b').with_stem, '') self.assertRaises(ValueError, P('a/b').with_stem, '/c') self.assertRaises(ValueError, P('a/b').with_stem, 'c/') self.assertRaises(ValueError, P('a/b').with_stem, 'c/d') def test_with_suffix_common(self): P = self.cls self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('a/b').with_suffix, '/') self.assertRaises(ValueError, P('a/b').with_suffix, '.') self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d') self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(ValueError, P('a/b').with_suffix, (self.flavour.sep, 'd')) def test_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.relative_to) self.assertRaises(TypeError, p.relative_to, b'a') self.assertEqual(p.relative_to(P()), P('a/b')) self.assertEqual(p.relative_to(''), P('a/b')) self.assertEqual(p.relative_to(P('a')), P('b')) self.assertEqual(p.relative_to('a'), P('b')) self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to('a/b'), P()) # With several args. self.assertEqual(p.relative_to('a', 'b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('c')) self.assertRaises(ValueError, p.relative_to, P('a/b/c')) self.assertRaises(ValueError, p.relative_to, P('a/c')) self.assertRaises(ValueError, p.relative_to, P('/a')) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) self.assertEqual(p.relative_to(P('/a')), P('b')) self.assertEqual(p.relative_to('/a'), P('b')) self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to('/a/b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) self.assertRaises(ValueError, p.relative_to, P('/a/c')) self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('a')) def test_is_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.is_relative_to) self.assertRaises(TypeError, p.is_relative_to, b'a') self.assertTrue(p.is_relative_to(P())) self.assertTrue(p.is_relative_to('')) self.assertTrue(p.is_relative_to(P('a'))) self.assertTrue(p.is_relative_to('a/')) self.assertTrue(p.is_relative_to(P('a/b'))) self.assertTrue(p.is_relative_to('a/b')) # With several args. self.assertTrue(p.is_relative_to('a', 'b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('c'))) self.assertFalse(p.is_relative_to(P('a/b/c'))) self.assertFalse(p.is_relative_to(P('a/c'))) self.assertFalse(p.is_relative_to(P('/a'))) p = P('/a/b') self.assertTrue(p.is_relative_to(P('/'))) self.assertTrue(p.is_relative_to('/')) self.assertTrue(p.is_relative_to(P('/a'))) self.assertTrue(p.is_relative_to('/a')) self.assertTrue(p.is_relative_to('/a/')) self.assertTrue(p.is_relative_to(P('/a/b'))) self.assertTrue(p.is_relative_to('/a/b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/c'))) self.assertFalse(p.is_relative_to(P('/a/b/c'))) self.assertFalse(p.is_relative_to(P('/a/c'))) self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) def test_pickling_common(self): P = self.cls p = P('/a/b') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertIs(pp.__class__, p.__class__) self.assertEqual(pp, p) self.assertEqual(hash(pp), hash(p)) self.assertEqual(str(pp), str(p)) class PurePosixPathTest(_BasePurePathTest): cls = pathlib.PurePosixPath def test_root(self): P = self.cls self.assertEqual(P('/a/b').root, '/') self.assertEqual(P('///a/b').root, '/') # POSIX special case for two leading slashes. self.assertEqual(P('//a/b').root, '//') def test_eq(self): P = self.cls self.assertNotEqual(P('a/b'), P('A/b')) self.assertEqual(P('/a'), P('///a')) self.assertNotEqual(P('/a'), P('//a')) def test_as_uri(self): P = self.cls self.assertEqual(P('/').as_uri(), 'file:///') self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c') self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c') def test_as_uri_non_ascii(self): from urllib.parse import quote_from_bytes P = self.cls try: os.fsencode('\xe9') except UnicodeEncodeError: self.skipTest("\\xe9 cannot be encoded to the filesystem encoding") self.assertEqual(P('/a/b\xe9').as_uri(), 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9'))) def test_match(self): P = self.cls self.assertFalse(P('A.py').match('a.PY')) def test_is_absolute(self): P = self.cls self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertTrue(P('/').is_absolute()) self.assertTrue(P('/a').is_absolute()) self.assertTrue(P('/a/b/').is_absolute()) self.assertTrue(P('//a').is_absolute()) self.assertTrue(P('//a/b').is_absolute()) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved()) def test_join(self): P = self.cls p = P('//a') pp = p.joinpath('b') self.assertEqual(pp, P('//a/b')) pp = P('/a').joinpath('//c') self.assertEqual(pp, P('//c')) pp = P('//a').joinpath('/c') self.assertEqual(pp, P('/c')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) class PureWindowsPathTest(_BasePurePathTest): cls = pathlib.PureWindowsPath equivalences = _BasePurePathTest.equivalences.copy() equivalences.update({ 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ], 'c:/a': [ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'), ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'), ], '//a/b/': [ ('//a/b',) ], '//a/b/c': [ ('//a/b', 'c'), ('//a/b/', 'c'), ], }) def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') p = self.cls('c:/a/b/c') self.assertEqual(str(p), 'c:\\a\\b\\c') p = self.cls('//a/b') self.assertEqual(str(p), '\\\\a\\b\\') p = self.cls('//a/b/c') self.assertEqual(str(p), '\\\\a\\b\\c') p = self.cls('//a/b/c/d') self.assertEqual(str(p), '\\\\a\\b\\c\\d') def test_str_subclass(self): self._check_str_subclass('c:') self._check_str_subclass('c:a') self._check_str_subclass('c:a\\b.txt') self._check_str_subclass('c:\\') self._check_str_subclass('c:\\a') self._check_str_subclass('c:\\a\\b.txt') self._check_str_subclass('\\\\some\\share') self._check_str_subclass('\\\\some\\share\\a') self._check_str_subclass('\\\\some\\share\\a\\b.txt') def test_eq(self): P = self.cls self.assertEqual(P('c:a/b'), P('c:a/b')) self.assertEqual(P('c:a/b'), P('c:', 'a', 'b')) self.assertNotEqual(P('c:a/b'), P('d:a/b')) self.assertNotEqual(P('c:a/b'), P('c:/a/b')) self.assertNotEqual(P('/a/b'), P('c:/a/b')) # Case-insensitivity. self.assertEqual(P('a/B'), P('A/b')) self.assertEqual(P('C:a/B'), P('c:A/b')) self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b')) def test_as_uri(self): P = self.cls with self.assertRaises(ValueError): P('/a/b').as_uri() with self.assertRaises(ValueError): P('c:a/b').as_uri() self.assertEqual(P('c:/').as_uri(), 'file:///c:/') self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c') self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c') self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9') self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/') self.assertEqual(P('//some/share/a/b.c').as_uri(), 'file://some/share/a/b.c') self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(), 'file://some/share/a/b%25%23c%C3%A9') def test_match_common(self): P = self.cls # Absolute patterns. self.assertTrue(P('c:/b.py').match('/*.py')) self.assertTrue(P('c:/b.py').match('c:*.py')) self.assertTrue(P('c:/b.py').match('c:/*.py')) self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('b.py').match('c:*.py')) self.assertFalse(P('b.py').match('c:/*.py')) self.assertFalse(P('c:b.py').match('/*.py')) self.assertFalse(P('c:b.py').match('c:/*.py')) self.assertFalse(P('/b.py').match('c:*.py')) self.assertFalse(P('/b.py').match('c:/*.py')) # UNC patterns. self.assertTrue(P('//some/share/a.py').match('/*.py')) self.assertTrue(P('//some/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//other/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py')) # Case-insensitivity. self.assertTrue(P('B.py').match('b.PY')) self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY')) self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY')) def test_ordering_common(self): # Case-insensitivity. def assertOrderedEqual(a, b): self.assertLessEqual(a, b) self.assertGreaterEqual(b, a) P = self.cls p = P('c:A/b') q = P('C:a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) p = P('//some/Share/A/b') q = P('//Some/SHARE/a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) def test_parts(self): P = self.cls p = P('c:a/b') parts = p.parts self.assertEqual(parts, ('c:', 'a', 'b')) p = P('c:/a/b') parts = p.parts self.assertEqual(parts, ('c:\\', 'a', 'b')) p = P('//a/b/c/d') parts = p.parts self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd')) def test_parent(self): # Anchored P = self.cls p = P('z:a/b/c') self.assertEqual(p.parent, P('z:a/b')) self.assertEqual(p.parent.parent, P('z:a')) self.assertEqual(p.parent.parent.parent, P('z:')) self.assertEqual(p.parent.parent.parent.parent, P('z:')) p = P('z:/a/b/c') self.assertEqual(p.parent, P('z:/a/b')) self.assertEqual(p.parent.parent, P('z:/a')) self.assertEqual(p.parent.parent.parent, P('z:/')) self.assertEqual(p.parent.parent.parent.parent, P('z:/')) p = P('//a/b/c/d') self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) def test_parents(self): # Anchored P = self.cls p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:a')) self.assertEqual(par[1], P('z:')) self.assertEqual(par[0:1], (P('z:a'),)) self.assertEqual(par[:-1], (P('z:a'),)) self.assertEqual(par[:2], (P('z:a'), P('z:'))) self.assertEqual(par[1:], (P('z:'),)) self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:/a')) self.assertEqual(par[1], P('z:/')) self.assertEqual(par[0:1], (P('z:/a'),)) self.assertEqual(par[0:-1], (P('z:/a'),)) self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) self.assertEqual(par[1:], (P('z:/'),)) self.assertEqual(par[::2], (P('z:/a'),)) self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('//a/b/c/d') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('//a/b/c')) self.assertEqual(par[1], P('//a/b')) self.assertEqual(par[0:1], (P('//a/b/c'),)) self.assertEqual(par[0:-1], (P('//a/b/c'),)) self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b'))) self.assertEqual(par[1:], (P('//a/b'),)) self.assertEqual(par[::2], (P('//a/b/c'),)) self.assertEqual(par[::-1], (P('//a/b'), P('//a/b/c'))) self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')]) with self.assertRaises(IndexError): par[2] def test_drive(self): P = self.cls self.assertEqual(P('c:').drive, 'c:') self.assertEqual(P('c:a/b').drive, 'c:') self.assertEqual(P('c:/').drive, 'c:') self.assertEqual(P('c:/a/b/').drive, 'c:') self.assertEqual(P('//a/b').drive, '\\\\a\\b') self.assertEqual(P('//a/b/').drive, '\\\\a\\b') self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b') def test_root(self): P = self.cls self.assertEqual(P('c:').root, '') self.assertEqual(P('c:a/b').root, '') self.assertEqual(P('c:/').root, '\\') self.assertEqual(P('c:/a/b/').root, '\\') self.assertEqual(P('//a/b').root, '\\') self.assertEqual(P('//a/b/').root, '\\') self.assertEqual(P('//a/b/c/d').root, '\\') def test_anchor(self): P = self.cls self.assertEqual(P('c:').anchor, 'c:') self.assertEqual(P('c:a/b').anchor, 'c:') self.assertEqual(P('c:/').anchor, 'c:\\') self.assertEqual(P('c:/a/b/').anchor, 'c:\\') self.assertEqual(P('//a/b').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/c/d').anchor, '\\\\a\\b\\') def test_name(self): P = self.cls self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') self.assertEqual(P('c:/a/.hgrc').suffix, '') self.assertEqual(P('c:a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) self.assertEqual(P('c:/a/.hgrc').suffixes, []) self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('//My.py/Share.php').suffixes, []) self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) def test_stem(self): P = self.cls self.assertEqual(P('c:').stem, '') self.assertEqual(P('c:.').stem, '') self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name(self): P = self.cls self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_name, '//My/Share') def test_with_stem(self): P = self.cls self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share') def test_with_suffix(self): P = self.cls self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d') def test_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:foO')), P('Bar')) self.assertEqual(p.relative_to('c:foO'), P('Bar')) self.assertEqual(p.relative_to('c:foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:foO/baR')), P()) self.assertEqual(p.relative_to('c:foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('Foo')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) p = P('C:/Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('/Foo/Bar')) self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar') self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar') self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:/foO')), P('Bar')) self.assertEqual(p.relative_to('c:/foO'), P('Bar')) self.assertEqual(p.relative_to('c:/foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:/foO/baR')), P()) self.assertEqual(p.relative_to('c:/foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo')) self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('d:/')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('//C/Foo')) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare'), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo')), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo'), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo')) def test_is_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertTrue(p.is_relative_to(P('c:'))) self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:foO'))) self.assertTrue(p.is_relative_to('c:foO')) self.assertTrue(p.is_relative_to('c:foO/')) self.assertTrue(p.is_relative_to(P('c:foO/baR'))) self.assertTrue(p.is_relative_to('c:foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('Foo'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('C:/Foo'))) self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo/Baz'))) p = P('C:/Foo/Bar') self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:/'))) self.assertTrue(p.is_relative_to(P('c:/foO'))) self.assertTrue(p.is_relative_to('c:/foO/')) self.assertTrue(p.is_relative_to(P('c:/foO/baR'))) self.assertTrue(p.is_relative_to('c:/foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('C:/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo'))) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('d:/'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('//C/Foo'))) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertTrue(p.is_relative_to(P('//sErver/sHare'))) self.assertTrue(p.is_relative_to('//sErver/sHare')) self.assertTrue(p.is_relative_to('//sErver/sHare/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo')) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo/Bar'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/Bar')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('c:/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//z/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//Server/z/Foo'))) def test_is_absolute(self): P = self.cls # Under NT, only paths with both a drive and a root are absolute. self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertFalse(P('/').is_absolute()) self.assertFalse(P('/a').is_absolute()) self.assertFalse(P('/a/b/').is_absolute()) self.assertFalse(P('c:').is_absolute()) self.assertFalse(P('c:a').is_absolute()) self.assertFalse(P('c:a/b/').is_absolute()) self.assertTrue(P('c:/').is_absolute()) self.assertTrue(P('c:/a').is_absolute()) self.assertTrue(P('c:/a/b/').is_absolute()) # UNC paths are absolute by definition. self.assertTrue(P('//a/b').is_absolute()) self.assertTrue(P('//a/b/').is_absolute()) self.assertTrue(P('//a/b/c').is_absolute()) self.assertTrue(P('//a/b/c/d').is_absolute()) def test_join(self): P = self.cls p = P('C:/a/b') pp = p.joinpath('x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. pp = p.joinpath('D:x/y') self.assertEqual(pp, P('D:x/y')) pp = p.joinpath('D:/x/y') self.assertEqual(pp, P('D:/x/y')) pp = p.joinpath('//host/share/x/y') self.assertEqual(pp, P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. pp = p.joinpath('c:x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('c:/x/y') self.assertEqual(pp, P('C:/x/y')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('C:/a/b') self.assertEqual(p / 'x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'x' / 'y', P('C:/a/b/x/y')) self.assertEqual(p / '/x/y', P('C:/x/y')) self.assertEqual(p / '/x' / 'y', P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. self.assertEqual(p / 'D:x/y', P('D:x/y')) self.assertEqual(p / 'D:' / 'x/y', P('D:x/y')) self.assertEqual(p / 'D:/x/y', P('D:/x/y')) self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y')) self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'c:/x/y', P('C:/x/y')) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) # UNC paths are never reserved. self.assertIs(False, P('//my/share/nul/con/aux').is_reserved()) # Case-insensitive DOS-device names are reserved. self.assertIs(True, P('nul').is_reserved()) self.assertIs(True, P('aux').is_reserved()) self.assertIs(True, P('prn').is_reserved()) self.assertIs(True, P('con').is_reserved()) self.assertIs(True, P('conin$').is_reserved()) self.assertIs(True, P('conout$').is_reserved()) # COM/LPT + 1-9 or + superscript 1-3 are reserved. self.assertIs(True, P('COM1').is_reserved()) self.assertIs(True, P('LPT9').is_reserved()) self.assertIs(True, P('com\xb9').is_reserved()) self.assertIs(True, P('com\xb2').is_reserved()) self.assertIs(True, P('lpt\xb3').is_reserved()) # DOS-device name mataching ignores characters after a dot or # a colon and also ignores trailing spaces. self.assertIs(True, P('NUL.txt').is_reserved()) self.assertIs(True, P('PRN ').is_reserved()) self.assertIs(True, P('AUX .txt').is_reserved()) self.assertIs(True, P('COM1:bar').is_reserved()) self.assertIs(True, P('LPT9 :bar').is_reserved()) # DOS-device names are only matched at the beginning # of a path component. self.assertIs(False, P('bar.com9').is_reserved()) self.assertIs(False, P('bar.lpt9').is_reserved()) # Only the last path component matters. self.assertIs(True, P('c:/baz/con/NUL').is_reserved()) self.assertIs(False, P('c:/NUL/con/baz').is_reserved()) class PurePathTest(_BasePurePathTest): cls = pathlib.PurePath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath) def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') self.assertNotEqual(p, q) def test_different_flavours_unordered(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') with self.assertRaises(TypeError): p < q with self.assertRaises(TypeError): p <= q with self.assertRaises(TypeError): p > q with self.assertRaises(TypeError): p >= q # # Tests for the concrete classes. # # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) rel_join = lambda *x: os.path.join(TESTFN, *x) only_nt = unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system') only_posix = unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system') @only_posix class PosixPathAsPureTest(PurePosixPathTest, unittest.TestCase): cls = PosixUPath @only_nt class WindowsPathAsPureTest(PureWindowsPathTest, unittest.TestCase): cls = WindowsUPath def test_owner(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').owner() def test_group(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').group() class _BasePathTest(object): """Tests for the FS-accessing functionalities of the Path classes.""" # (BASE) # | # |-- brokenLink -> non-existing # |-- dirA # | `-- linkC -> ../dirB # |-- dirB # | |-- fileB # | `-- linkD -> ../dirB # |-- dirC # | |-- dirD # | | `-- fileD # | `-- fileC # | `-- novel.txt # |-- dirE # No permissions # |-- fileA # |-- linkA -> fileA # |-- linkB -> dirB # `-- brokenLinkLoop -> brokenLinkLoop # def setUp(self): def cleanup(): os.chmod(join('dirE'), 0o777) os_helper.rmtree(BASE) self.addCleanup(cleanup) os.mkdir(BASE) os.mkdir(join('dirA')) os.mkdir(join('dirB')) os.mkdir(join('dirC')) os.mkdir(join('dirC', 'dirD')) os.mkdir(join('dirE')) with open(join('fileA'), 'wb') as f: f.write(b"this is file A\n") with open(join('dirB', 'fileB'), 'wb') as f: f.write(b"this is file B\n") with open(join('dirC', 'fileC'), 'wb') as f: f.write(b"this is file C\n") with open(join('dirC', 'novel.txt'), 'wb') as f: f.write(b"this is a novel\n") with open(join('dirC', 'dirD', 'fileD'), 'wb') as f: f.write(b"this is file D\n") os.chmod(join('dirE'), 0) if os_helper.can_symlink(): # Relative symlinks. os.symlink('fileA', join('linkA')) os.symlink('non-existing', join('brokenLink')) self.dirlink('dirB', join('linkB')) self.dirlink(os.path.join('..', 'dirB'), join('dirA', 'linkC')) # This one goes upwards, creating a loop. self.dirlink(os.path.join('..', 'dirB'), join('dirB', 'linkD')) # Broken symlink (pointing to itself). os.symlink('brokenLinkLoop', join('brokenLinkLoop')) if os.name == 'nt': # Workaround for http://bugs.python.org/issue13772. def dirlink(self, src, dest): os.symlink(src, dest, target_is_directory=True) else: def dirlink(self, src, dest): os.symlink(src, dest) def assertSame(self, path_a, path_b): self.assertTrue(os.path.samefile(str(path_a), str(path_b)), "%r and %r don't point to the same file" % (path_a, path_b)) def assertFileNotFound(self, func, *args, **kwargs): with self.assertRaises(FileNotFoundError) as cm: func(*args, **kwargs) self.assertEqual(cm.exception.errno, errno.ENOENT) def assertEqualNormCase(self, path_a, path_b): self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b)) def _test_cwd(self, p): q = self.cls(os.getcwd()) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_cwd(self): p = self.cls.cwd() self._test_cwd(p) def test_absolute_common(self): P = self.cls with mock.patch("os.getcwd") as getcwd: getcwd.return_value = BASE # Simple relative paths. self.assertEqual(str(P().absolute()), BASE) self.assertEqual(str(P('.').absolute()), BASE) self.assertEqual(str(P('a').absolute()), os.path.join(BASE, 'a')) self.assertEqual(str(P('a', 'b', 'c').absolute()), os.path.join(BASE, 'a', 'b', 'c')) # Symlinks should not be resolved. self.assertEqual(str(P('linkB', 'fileB').absolute()), os.path.join(BASE, 'linkB', 'fileB')) self.assertEqual(str(P('brokenLink').absolute()), os.path.join(BASE, 'brokenLink')) self.assertEqual(str(P('brokenLinkLoop').absolute()), os.path.join(BASE, 'brokenLinkLoop')) # '..' entries should be preserved and not normalised. self.assertEqual(str(P('..').absolute()), os.path.join(BASE, '..')) self.assertEqual(str(P('a', '..').absolute()), os.path.join(BASE, 'a', '..')) self.assertEqual(str(P('..', 'b').absolute()), os.path.join(BASE, '..', 'b')) def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) @unittest.skipIf( pwd is None, reason="Test requires pwd module to get homedir." ) def test_home(self): with os_helper.EnvironmentVarGuard() as env: self._test_home(self.cls.home()) env.clear() env['USERPROFILE'] = os.path.join(BASE, 'userprofile') self._test_home(self.cls.home()) # bpo-38883: ignore `HOME` when set on windows env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) def test_samefile(self): fileA_path = os.path.join(BASE, 'fileA') fileB_path = os.path.join(BASE, 'dirB', 'fileB') p = self.cls(fileA_path) pp = self.cls(fileA_path) q = self.cls(fileB_path) self.assertTrue(p.samefile(fileA_path)) self.assertTrue(p.samefile(pp)) self.assertFalse(p.samefile(fileB_path)) self.assertFalse(p.samefile(q)) # Test the non-existent file case non_existent = os.path.join(BASE, 'foo') r = self.cls(non_existent) self.assertRaises(FileNotFoundError, p.samefile, r) self.assertRaises(FileNotFoundError, p.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, p) self.assertRaises(FileNotFoundError, r.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, r) self.assertRaises(FileNotFoundError, r.samefile, non_existent) def test_empty_path(self): # The empty path points to '.' p = self.cls('') self.assertEqual(p.stat(), os.stat('.')) @unittest.skipIf(is_wasi, "WASI has no user accounts.") def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') self.assertEqual(p.expanduser(), p) p = P('../~') self.assertEqual(p.expanduser(), p) p = P(P('').absolute().anchor) / '~' self.assertEqual(p.expanduser(), p) def test_exists(self): P = self.cls p = P(BASE) self.assertIs(True, p.exists()) self.assertIs(True, (p / 'dirA').exists()) self.assertIs(True, (p / 'fileA').exists()) self.assertIs(False, (p / 'fileA' / 'bah').exists()) if os_helper.can_symlink(): self.assertIs(True, (p / 'linkA').exists()) self.assertIs(True, (p / 'linkB').exists()) self.assertIs(True, (p / 'linkB' / 'fileB').exists()) self.assertIs(False, (p / 'linkA' / 'bah').exists()) self.assertIs(False, (p / 'foo').exists()) self.assertIs(False, P('/xyzzy').exists()) self.assertIs(False, P(BASE + '\udfff').exists()) self.assertIs(False, P(BASE + '\x00').exists()) def test_open_common(self): p = self.cls(BASE) with (p / 'fileA').open('r') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), "this is file A\n") with (p / 'fileA').open('rb') as f: self.assertIsInstance(f, io.BufferedIOBase) self.assertEqual(f.read().strip(), b"this is file A") with (p / 'fileA').open('rb', buffering=0) as f: self.assertIsInstance(f, io.RawIOBase) self.assertEqual(f.read().strip(), b"this is file A") def test_read_write_bytes(self): p = self.cls(BASE) (p / 'fileA').write_bytes(b'abcdefg') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') # Check that trying to write str does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') def test_read_write_text(self): p = self.cls(BASE) (p / 'fileA').write_text('äbcdefg', encoding='latin-1') self.assertEqual((p / 'fileA').read_text( encoding='utf-8', errors='ignore'), 'bcdefg') # Check that trying to write bytes does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes') self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg') def test_write_text_with_newlines(self): p = self.cls(BASE) # Check that `\n` character change nothing (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\nfghlk\n\rmnopq') # Check that `\r` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\rfghlk\r\rmnopq') # Check that `\r\n` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\r\nfghlk\r\n\rmnopq') # Check that no argument passed will change `\n` to `os.linesep` os_linesep_byte = bytes(os.linesep, encoding='ascii') (p / 'fileA').write_text('abcde\nfghlk\n\rmnopq') self.assertEqual((p / 'fileA').read_bytes(), b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq') def test_iterdir(self): P = self.cls p = P(BASE) it = p.iterdir() paths = set(it) expected = ['dirA', 'dirB', 'dirC', 'dirE', 'fileA'] if os_helper.can_symlink(): expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop'] self.assertEqual(paths, { P(BASE, q) for q in expected }) @os_helper.skip_unless_symlink def test_iterdir_symlink(self): # __iter__ on a symlink to a directory. P = self.cls p = P(BASE, 'linkB') paths = set(p.iterdir()) expected = { P(BASE, 'linkB', q) for q in ['fileB', 'linkD'] } self.assertEqual(paths, expected) def test_iterdir_nodir(self): # __iter__ on something that is not a directory. p = self.cls(BASE, 'fileA') with self.assertRaises(OSError) as cm: next(p.iterdir()) # ENOENT or EINVAL under Windows, ENOTDIR otherwise # (see issue #12802). self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) def test_glob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.glob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.glob("fileB"), []) _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) else: _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) if not os_helper.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not os_helper.can_symlink(): _check(p.glob("*/fileB"), ['dirB/fileB']) else: _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) if not os_helper.can_symlink(): _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) else: _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) def test_rglob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.rglob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) if not os_helper.can_symlink(): _check(p.rglob("*/fileB"), ["dirB/fileB"]) else: _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) if not os_helper.can_symlink(): _check(p.rglob("*/"), [ "dirA", "dirB", "dirC", "dirC/dirD", "dirE", ]) else: _check(p.rglob("*/"), [ "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", "dirC/dirD", "dirE", "linkB", ]) _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") _check(p.rglob("*"), ["dirC/fileC", "dirC/novel.txt", "dirC/dirD", "dirC/dirD/fileD"]) _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) _check(p.rglob("*/"), ["dirC/dirD"]) _check(p.rglob(""), ["dirC", "dirC/dirD"]) # gh-91616, a re module regression _check(p.rglob("*.txt"), ["dirC/novel.txt"]) _check(p.rglob("*.*"), ["dirC/novel.txt"]) @os_helper.skip_unless_symlink def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) given = set(p.rglob('*')) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', 'dirC', 'dirC/dirD', 'dirC/dirD/fileD', 'dirC/fileC', 'dirC/novel.txt', 'dirE', 'fileA', 'linkA', 'linkB', 'brokenLinkLoop', } self.assertEqual(given, {p / x for x in expect}) def test_glob_many_open_files(self): depth = 30 P = self.cls base = P(BASE) / 'deep' p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) iters = [base.glob(pattern) for j in range(100)] for it in iters: self.assertEqual(next(it), p) iters = [base.rglob('d') for j in range(100)] p = base for i in range(depth): p = p / 'd' for it in iters: self.assertEqual(next(it), p) def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) self.assertEqual(set(p.glob("../xyzzy")), set()) @os_helper.skip_unless_symlink def test_glob_permissions(self): # See bpo-38894 P = self.cls base = P(BASE) / 'permissions' base.mkdir() file1 = base / "file1" file1.touch() file2 = base / "file2" file2.touch() subdir = base / "subdir" file3 = base / "file3" file3.symlink_to(subdir / "other") # Patching is needed to avoid relying on the filesystem # to return the order of the files as the error will not # happen if the symlink is the last item. real_scandir = os.scandir def my_scandir(path): with real_scandir(path) as scandir_it: entries = list(scandir_it) entries.sort(key=lambda entry: entry.name) return contextlib.nullcontext(entries) with mock.patch("os.scandir", my_scandir): self.assertEqual(len(set(base.glob("*"))), 3) subdir.mkdir() self.assertEqual(len(set(base.glob("*"))), 4) subdir.chmod(000) self.assertEqual(len(set(base.glob("*"))), 4) @os_helper.skip_unless_symlink def test_glob_long_symlink(self): # See gh-87695 base = self.cls(BASE) / 'long_symlink' base.mkdir() bad_link = base / 'bad_link' bad_link.symlink_to("bad" * 200) self.assertEqual(sorted(base.glob('**/*')), [bad_link]) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) self.assertEqual(q, expected) # This can be used to check both relative and absolute resolutions. _check_resolve_relative = _check_resolve_absolute = _check_resolve @os_helper.skip_unless_symlink def test_resolve_common(self): P = self.cls p = P(BASE, 'foo') with self.assertRaises(OSError) as cm: p.resolve(strict=True) self.assertEqual(cm.exception.errno, errno.ENOENT) # Non-strict self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo')) p = P(BASE, 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo', 'in', 'spam')) p = P(BASE, '..', 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.abspath(os.path.join('foo', 'in', 'spam'))) # These are all relative symlinks. p = P(BASE, 'dirB', 'fileB') self._check_resolve_relative(p, p) p = P(BASE, 'linkA') self._check_resolve_relative(p, P(BASE, 'fileA')) p = P(BASE, 'dirA', 'linkC', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) p = P(BASE, 'dirB', 'linkD', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(BASE, 'dirA', 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) # Now create absolute symlinks. d = os_helper._longpath(tempfile.mkdtemp(suffix='-dirD', dir=os.getcwd())) self.addCleanup(os_helper.rmtree, d) os.symlink(os.path.join(d), join('dirA', 'linkX')) os.symlink(join('dirB'), os.path.join(d, 'linkY')) p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB') self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) @os_helper.skip_unless_symlink def test_resolve_dot(self): # See https://bitbucket.org/pitrou/pathlib/issue/9/pathresolve-fails-on-complex-symlinks p = self.cls(BASE) self.dirlink('.', join('0')) self.dirlink(os.path.join('0', '0'), join('1')) self.dirlink(os.path.join('1', '1'), join('2')) q = p / '2' self.assertEqual(q.resolve(strict=True), p) r = q / '3' / '4' self.assertRaises(FileNotFoundError, r.resolve, strict=True) # Non-strict self.assertEqual(r.resolve(strict=False), p / '3' / '4') def test_resolve_nonexist_relative_issue38671(self): p = self.cls('non', 'exist') old_cwd = os.getcwd() os.chdir(BASE) try: self.assertEqual(p.resolve(), self.cls(BASE, p)) finally: os.chdir(old_cwd) def test_with(self): p = self.cls(BASE) it = p.iterdir() it2 = p.iterdir() next(it2) # bpo-46556: path context managers are deprecated in Python 3.11. with self.assertWarns(DeprecationWarning): with p: pass # Using a path as a context manager is a no-op, thus the following # operations should still succeed after the context manage exits. next(it) next(it2) p.exists() p.resolve() p.absolute() with self.assertWarns(DeprecationWarning): with p: pass @os_helper.skip_unless_working_chmod def test_chmod(self): p = self.cls(BASE) / 'fileA' mode = p.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # Set writable bit. new_mode = mode | 0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # On Windows, os.chmod does not follow symlinks (issue #15411) @only_posix @os_helper.skip_unless_working_chmod def test_chmod_follow_symlinks_true(self): p = self.cls(BASE) / 'linkA' q = p.resolve() mode = q.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # Set writable bit new_mode = mode | 0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # XXX also need a test for lchmod. @os_helper.skip_unless_working_chmod def test_stat(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(p.stat(), st) # Change file mode by flipping write bit. p.chmod(st.st_mode ^ 0o222) self.addCleanup(p.chmod, st.st_mode) self.assertNotEqual(p.stat(), st) @os_helper.skip_unless_symlink def test_stat_no_follow_symlinks(self): p = self.cls(BASE) / 'linkA' st = p.stat() self.assertNotEqual(st, p.stat(follow_symlinks=False)) def test_stat_no_follow_symlinks_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.stat(follow_symlinks=False)) @os_helper.skip_unless_symlink def test_lstat(self): p = self.cls(BASE)/ 'linkA' st = p.stat() self.assertNotEqual(st, p.lstat()) def test_lstat_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.lstat()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") def test_owner(self): p = self.cls(BASE) / 'fileA' uid = p.stat().st_uid try: name = pwd.getpwuid(uid).pw_name except KeyError: self.skipTest( "user %d doesn't have an entry in the system database" % uid) self.assertEqual(name, p.owner()) @unittest.skipUnless(grp, "the grp module is needed for this test") def test_group(self): p = self.cls(BASE) / 'fileA' gid = p.stat().st_gid try: name = grp.getgrgid(gid).gr_name except KeyError: self.skipTest( "group %d doesn't have an entry in the system database" % gid) self.assertEqual(name, p.group()) def test_unlink(self): p = self.cls(BASE) / 'fileA' p.unlink() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) def test_unlink_missing_ok(self): p = self.cls(BASE) / 'fileAAA' self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self): p = self.cls(BASE) / 'dirA' for q in p.iterdir(): q.unlink() p.rmdir() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_link_to(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # linking to another path. q = P / 'dirA' / 'fileAA' try: with self.assertWarns(DeprecationWarning): p.link_to(q) except PermissionError as e: self.skipTest('os.link(): %s' % e) self.assertEqual(q.stat().st_size, size) self.assertEqual(os.path.samefile(p, q), True) self.assertTrue(p.stat) # Linking to a str of a relative path. r = rel_join('fileAAA') with self.assertWarns(DeprecationWarning): q.link_to(r) self.assertEqual(os.stat(r).st_size, size) self.assertTrue(q.stat) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_hardlink_to(self): P = self.cls(BASE) target = P / 'fileA' size = target.stat().st_size # linking to another path. link = P / 'dirA' / 'fileAA' link.hardlink_to(target) self.assertEqual(link.stat().st_size, size) self.assertTrue(os.path.samefile(target, link)) self.assertTrue(target.exists()) # Linking to a str of a relative path. link2 = P / 'dirA' / 'fileAAA' target2 = rel_join('fileA') link2.hardlink_to(target2) self.assertEqual(os.stat(target2).st_size, size) self.assertTrue(link2.exists()) @unittest.skipIf(hasattr(os, "link"), "os.link() is present") def test_link_to_not_implemented(self): P = self.cls(BASE) p = P / 'fileA' # linking to another path. q = P / 'dirA' / 'fileAA' with self.assertRaises(NotImplementedError): p.link_to(q) def test_rename(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Renaming to another path. q = P / 'dirA' / 'fileAA' renamed_p = p.rename(q) self.assertEqual(renamed_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Renaming to a str of a relative path. r = rel_join('fileAAA') renamed_q = q.rename(r) self.assertEqual(renamed_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) def test_replace(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Replacing a non-existing path. q = P / 'dirA' / 'fileAA' replaced_p = p.replace(q) self.assertEqual(replaced_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Replacing another (existing) path. r = rel_join('dirB', 'fileB') replaced_q = q.replace(r) self.assertEqual(replaced_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) @os_helper.skip_unless_symlink def test_readlink(self): P = self.cls(BASE) self.assertEqual((P / 'linkA').readlink(), self.cls('fileA')) self.assertEqual((P / 'brokenLink').readlink(), self.cls('non-existing')) self.assertEqual((P / 'linkB').readlink(), self.cls('dirB')) with self.assertRaises(OSError): (P / 'fileA').readlink() def test_touch_common(self): P = self.cls(BASE) p = P / 'newfileA' self.assertFalse(p.exists()) p.touch() self.assertTrue(p.exists()) st = p.stat() old_mtime = st.st_mtime old_mtime_ns = st.st_mtime_ns # Rewind the mtime sufficiently far in the past to work around # filesystem-specific timestamp granularity. os.utime(str(p), (old_mtime - 10, old_mtime - 10)) # The file mtime should be refreshed by calling touch() again. p.touch() st = p.stat() self.assertGreaterEqual(st.st_mtime_ns, old_mtime_ns) self.assertGreaterEqual(st.st_mtime, old_mtime) # Now with exist_ok=False. p = P / 'newfileB' self.assertFalse(p.exists()) p.touch(mode=0o700, exist_ok=False) self.assertTrue(p.exists()) self.assertRaises(OSError, p.touch, exist_ok=False) def test_touch_nochange(self): P = self.cls(BASE) p = P / 'fileA' p.touch() with p.open('rb') as f: self.assertEqual(f.read().strip(), b"this is file A") def test_mkdir(self): P = self.cls(BASE) p = P / 'newdirA' self.assertFalse(p.exists()) p.mkdir() self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_parents(self): # Creating a chain of directories. p = self.cls(BASE, 'newdirB', 'newdirC') self.assertFalse(p.exists()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.ENOENT) p.mkdir(parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) # Test `mode` arg. mode = stat.S_IMODE(p.stat().st_mode) # Default mode. p = self.cls(BASE, 'newdirD', 'newdirE') p.mkdir(0o555, parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) if os.name != 'nt': # The directory's permissions follow the mode argument. self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o7555 & mode) # The parent's permissions follow the default process settings. self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode) def test_mkdir_exist_ok(self): p = self.cls(BASE, 'dirB') st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_with_parent(self): p = self.cls(BASE, 'dirC') self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p = p / 'newdirC' p.mkdir(parents=True) st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(parents=True, exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) @unittest.skipIf(is_emscripten, "FS root cannot be modified on Emscripten.") def test_mkdir_exist_ok_root(self): # Issue #25803: A drive root could raise PermissionError on Windows. self.cls('/').resolve().mkdir(exist_ok=True) self.cls('/').resolve().mkdir(parents=True, exist_ok=True) @only_nt # XXX: not sure how to test this on POSIX. def test_mkdir_with_unknown_drive(self): for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA': p = self.cls(d + ':\\') if not p.is_dir(): break else: self.skipTest("cannot find a drive that doesn't exist") with self.assertRaises(OSError): (p / 'child' / 'path').mkdir(parents=True) def test_mkdir_with_child_file(self): p = self.cls(BASE, 'dirB', 'fileB') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True, exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_no_parents_file(self): p = self.cls(BASE, 'fileA') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_concurrent_parent_creation(self): for pattern_num in range(32): p = self.cls(BASE, 'dirCPC%d' % pattern_num) self.assertFalse(p.exists()) real_mkdir = os.mkdir def my_mkdir(path, mode=0o777): path = str(path) # Emulate another process that would create the directory # just before we try to create it ourselves. We do it # in all possible pattern combinations, assuming that this # function is called at most 5 times (dirCPC/dir1/dir2, # dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2). if pattern.pop(): real_mkdir(path, mode) # From another process. concurrently_created.add(path) real_mkdir(path, mode) # Our real call. pattern = [bool(pattern_num & (1 << n)) for n in range(5)] concurrently_created = set() p12 = p / 'dir1' / 'dir2' try: with mock.patch("os.mkdir", my_mkdir): p12.mkdir(parents=True, exist_ok=False) except FileExistsError: self.assertIn(str(p12), concurrently_created) else: self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) @os_helper.skip_unless_symlink def test_symlink_to(self): P = self.cls(BASE) target = P / 'fileA' # Symlinking a path target. link = P / 'dirA' / 'linkAA' link.symlink_to(target) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) # Symlinking a str target. link = P / 'dirA' / 'linkAAA' link.symlink_to(str(target)) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertFalse(link.is_dir()) # Symlinking to a directory. target = P / 'dirB' link = P / 'dirA' / 'linkAAAA' link.symlink_to(target, target_is_directory=True) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertTrue(link.is_dir()) self.assertTrue(list(link.iterdir())) def test_is_dir(self): P = self.cls(BASE) self.assertTrue((P / 'dirA').is_dir()) self.assertFalse((P / 'fileA').is_dir()) self.assertFalse((P / 'non-existing').is_dir()) self.assertFalse((P / 'fileA' / 'bah').is_dir()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_dir()) self.assertTrue((P / 'linkB').is_dir()) self.assertFalse((P/ 'brokenLink').is_dir(), False) self.assertIs((P / 'dirA\udfff').is_dir(), False) self.assertIs((P / 'dirA\x00').is_dir(), False) def test_is_file(self): P = self.cls(BASE) self.assertTrue((P / 'fileA').is_file()) self.assertFalse((P / 'dirA').is_file()) self.assertFalse((P / 'non-existing').is_file()) self.assertFalse((P / 'fileA' / 'bah').is_file()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_file()) self.assertFalse((P / 'linkB').is_file()) self.assertFalse((P/ 'brokenLink').is_file()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) @only_posix def test_is_mount(self): P = self.cls(BASE) R = self.cls('/') # TODO: Work out Windows. self.assertFalse((P / 'fileA').is_mount()) self.assertFalse((P / 'dirA').is_mount()) self.assertFalse((P / 'non-existing').is_mount()) self.assertFalse((P / 'fileA' / 'bah').is_mount()) self.assertTrue(R.is_mount()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_mount()) self.assertIs(self.cls('/\udfff').is_mount(), False) self.assertIs(self.cls('/\x00').is_mount(), False) def test_is_symlink(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_symlink()) self.assertFalse((P / 'dirA').is_symlink()) self.assertFalse((P / 'non-existing').is_symlink()) self.assertFalse((P / 'fileA' / 'bah').is_symlink()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_symlink()) self.assertTrue((P / 'linkB').is_symlink()) self.assertTrue((P/ 'brokenLink').is_symlink()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) if os_helper.can_symlink(): self.assertIs((P / 'linkA\udfff').is_file(), False) self.assertIs((P / 'linkA\x00').is_file(), False) def test_is_fifo_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_fifo()) self.assertFalse((P / 'dirA').is_fifo()) self.assertFalse((P / 'non-existing').is_fifo()) self.assertFalse((P / 'fileA' / 'bah').is_fifo()) self.assertIs((P / 'fileA\udfff').is_fifo(), False) self.assertIs((P / 'fileA\x00').is_fifo(), False) @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required") @unittest.skipIf(sys.platform == "vxworks", "fifo requires special path on VxWorks") def test_is_fifo_true(self): P = self.cls(BASE, 'myfifo') try: os.mkfifo(str(P)) except PermissionError as e: self.skipTest('os.mkfifo(): %s' % e) self.assertTrue(P.is_fifo()) self.assertFalse(P.is_socket()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'myfifo\udfff').is_fifo(), False) self.assertIs(self.cls(BASE, 'myfifo\x00').is_fifo(), False) def test_is_socket_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_socket()) self.assertFalse((P / 'dirA').is_socket()) self.assertFalse((P / 'non-existing').is_socket()) self.assertFalse((P / 'fileA' / 'bah').is_socket()) self.assertIs((P / 'fileA\udfff').is_socket(), False) self.assertIs((P / 'fileA\x00').is_socket(), False) @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") @unittest.skipIf( is_emscripten, "Unix sockets are not implemented on Emscripten." ) @unittest.skipIf( is_wasi, "Cannot create socket on WASI." ) def test_is_socket_true(self): P = self.cls(BASE, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.addCleanup(sock.close) try: sock.bind(str(P)) except OSError as e: if (isinstance(e, PermissionError) or "AF_UNIX path too long" in str(e)): self.skipTest("cannot bind Unix socket: " + str(e)) self.assertTrue(P.is_socket()) self.assertFalse(P.is_fifo()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'mysock\udfff').is_socket(), False) self.assertIs(self.cls(BASE, 'mysock\x00').is_socket(), False) def test_is_block_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_block_device()) self.assertFalse((P / 'dirA').is_block_device()) self.assertFalse((P / 'non-existing').is_block_device()) self.assertFalse((P / 'fileA' / 'bah').is_block_device()) self.assertIs((P / 'fileA\udfff').is_block_device(), False) self.assertIs((P / 'fileA\x00').is_block_device(), False) def test_is_char_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_char_device()) self.assertFalse((P / 'dirA').is_char_device()) self.assertFalse((P / 'non-existing').is_char_device()) self.assertFalse((P / 'fileA' / 'bah').is_char_device()) self.assertIs((P / 'fileA\udfff').is_char_device(), False) self.assertIs((P / 'fileA\x00').is_char_device(), False) def test_is_char_device_true(self): # Under Unix, /dev/null should generally be a char device. P = self.cls('/dev/null') if not P.exists(): self.skipTest("/dev/null required") self.assertTrue(P.is_char_device()) self.assertFalse(P.is_block_device()) self.assertFalse(P.is_file()) self.assertIs(self.cls('/dev/null\udfff').is_char_device(), False) self.assertIs(self.cls('/dev/null\x00').is_char_device(), False) def test_pickling_common(self): p = self.cls(BASE, 'fileA') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertEqual(pp.stat(), p.stat()) def test_parts_interning(self): P = self.cls p = P('/usr/bin/foo') q = P('/usr/local/bin') # 'usr' self.assertIs(p.parts[1], q.parts[1]) # 'bin' self.assertIs(p.parts[2], q.parts[3]) def _check_complex_symlinks(self, link0_target): # Test solving a non-looping chain of symlinks (issue #19887). P = self.cls(BASE) self.dirlink(os.path.join('link0', 'link0'), join('link1')) self.dirlink(os.path.join('link1', 'link1'), join('link2')) self.dirlink(os.path.join('link2', 'link2'), join('link3')) self.dirlink(link0_target, join('link0')) # Resolve absolute paths. p = (P / 'link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) # Resolve relative paths. old_path = os.getcwd() os.chdir(BASE) try: p = self.cls('link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) finally: os.chdir(old_path) @os_helper.skip_unless_symlink def test_complex_symlinks_absolute(self): self._check_complex_symlinks(BASE) @os_helper.skip_unless_symlink def test_complex_symlinks_relative(self): self._check_complex_symlinks('.') @os_helper.skip_unless_symlink def test_complex_symlinks_relative_dot_dot(self): self._check_complex_symlinks(os.path.join('dirA', '..')) class PathTest(_BasePathTest, unittest.TestCase): cls = UPath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), WindowsUPath if os.name == 'nt' else PosixUPath) def test_unsupported_flavour(self): if os.name == 'nt': self.assertRaises(NotImplementedError, PosixUPath) else: self.assertRaises(NotImplementedError, WindowsUPath) def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): list(p.glob('')) @only_posix class PosixPathTest(_BasePathTest, unittest.TestCase): cls = PosixUPath def test_absolute(self): P = self.cls self.assertEqual(str(P('/').absolute()), '/') self.assertEqual(str(P('/a').absolute()), '/a') self.assertEqual(str(P('/a/b').absolute()), '/a/b') # '//'-prefixed absolute path (supported by POSIX). self.assertEqual(str(P('//').absolute()), '//') self.assertEqual(str(P('//a').absolute()), '//a') self.assertEqual(str(P('//a/b').absolute()), '//a/b') def _check_symlink_loop(self, *args, strict=True): path = self.cls(*args) with self.assertRaises(RuntimeError): print(path.resolve(strict)) @unittest.skipIf( is_emscripten or is_wasi, "umask is not implemented on Emscripten/WASI." ) def test_open_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) with (p / 'new_file').open('wb'): pass st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) with (p / 'other_new_file').open('wb'): pass st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) def test_resolve_root(self): current_directory = os.getcwd() try: os.chdir('/') p = self.cls('spam') self.assertEqual(str(p.resolve()), '/spam') finally: os.chdir(current_directory) @unittest.skipIf( is_emscripten or is_wasi, "umask is not implemented on Emscripten/WASI." ) def test_touch_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) (p / 'new_file').touch() st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) (p / 'other_new_file').touch() st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) (p / 'masked_new_file').touch(mode=0o750) st = os.stat(join('masked_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o750) @os_helper.skip_unless_symlink def test_resolve_loop(self): # Loops with relative symlinks. os.symlink('linkX/inside', join('linkX')) self._check_symlink_loop(BASE, 'linkX') os.symlink('linkY', join('linkY')) self._check_symlink_loop(BASE, 'linkY') os.symlink('linkZ/../linkZ', join('linkZ')) self._check_symlink_loop(BASE, 'linkZ') # Non-strict self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False) # Loops with absolute symlinks. os.symlink(join('linkU/inside'), join('linkU')) self._check_symlink_loop(BASE, 'linkU') os.symlink(join('linkV'), join('linkV')) self._check_symlink_loop(BASE, 'linkV') os.symlink(join('linkW/../linkW'), join('linkW')) self._check_symlink_loop(BASE, 'linkW') # Non-strict self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False) def test_glob(self): P = self.cls p = P(BASE) given = set(p.glob("FILEa")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.glob("FILEa*")), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") given = set(p.rglob("FILEd")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.rglob("FILEd*")), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') @unittest.skipIf(sys.platform == "vxworks", "no home directory on VxWorks") def test_expanduser(self): P = self.cls import_helper.import_module('pwd') import pwd pwdent = pwd.getpwuid(os.getuid()) username = pwdent.pw_name userhome = pwdent.pw_dir.rstrip('/') or '/' # Find arbitrary different user (if exists). for pwdent in pwd.getpwall(): othername = pwdent.pw_name otherhome = pwdent.pw_dir.rstrip('/') if othername != username and otherhome: break else: othername = username otherhome = userhome fakename = 'fakeuser' # This user can theoretically exist on a test runner. Create unique name: try: while pwd.getpwnam(fakename): fakename += '1' except KeyError: pass # Non-existent name found p1 = P('~/Documents') p2 = P(f'~{username}/Documents') p3 = P(f'~{othername}/Documents') p4 = P(f'../~{username}/Documents') p5 = P(f'/~{username}/Documents') p6 = P('') p7 = P(f'~{fakename}/Documents') with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) self.assertEqual(p1.expanduser(), P(userhome) / 'Documents') self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) env['HOME'] = '/tmp' self.assertEqual(p1.expanduser(), P('/tmp/Documents')) self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) @unittest.skipIf(sys.platform != "darwin", "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: file_descriptors = list(UPath('/dev/fd').rglob("*"))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee # which one will fail. for f in file_descriptors: f.exists() f.is_dir() f.is_file() f.is_symlink() f.is_block_device() f.is_char_device() f.is_fifo() f.is_socket() except OSError as e: if e.errno == errno.EBADF: self.fail("Bad file descriptor not handled.") raise @only_nt class WindowsPathTest(_BasePathTest, unittest.TestCase): cls = WindowsUPath def test_absolute(self): P = self.cls # Simple absolute paths. self.assertEqual(str(P('c:\\').absolute()), 'c:\\') self.assertEqual(str(P('c:\\a').absolute()), 'c:\\a') self.assertEqual(str(P('c:\\a\\b').absolute()), 'c:\\a\\b') # UNC absolute paths. share = '\\\\server\\share\\' self.assertEqual(str(P(share).absolute()), share) self.assertEqual(str(P(share + 'a').absolute()), share + 'a') self.assertEqual(str(P(share + 'a\\b').absolute()), share + 'a\\b') # UNC relative paths. with mock.patch("os.getcwd") as getcwd: getcwd.return_value = share self.assertEqual(str(P().absolute()), share) self.assertEqual(str(P('.').absolute()), share) self.assertEqual(str(P('a').absolute()), os.path.join(share, 'a')) self.assertEqual(str(P('a', 'b', 'c').absolute()), os.path.join(share, 'a', 'b', 'c')) def test_glob(self): P = self.cls p = P(BASE) self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) self.assertEqual(set(p.glob("*a\\")), { P(BASE, "dirA") }) self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\FILEa"}) self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(BASE, "dirC/dirD") }) self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\FILEd"}) def test_expanduser(self): P = self.cls with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) env.pop('USERPROFILE', None) env.pop('HOMEPATH', None) env.pop('HOMEDRIVE', None) env['USERNAME'] = 'alice' # test that the path returns unchanged p1 = P('~/My Documents') p2 = P('~alice/My Documents') p3 = P('~bob/My Documents') p4 = P('/~/My Documents') p5 = P('d:~/My Documents') p6 = P('') self.assertRaises(RuntimeError, p1.expanduser) self.assertRaises(RuntimeError, p2.expanduser) self.assertRaises(RuntimeError, p3.expanduser) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) def check(): env.pop('USERNAME', None) self.assertEqual(p1.expanduser(), P('C:/Users/alice/My Documents')) self.assertRaises(RuntimeError, p2.expanduser) env['USERNAME'] = 'alice' self.assertEqual(p2.expanduser(), P('C:/Users/alice/My Documents')) self.assertEqual(p3.expanduser(), P('C:/Users/bob/My Documents')) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) env['HOMEPATH'] = 'C:\\Users\\alice' check() env['HOMEDRIVE'] = 'C:\\' env['HOMEPATH'] = 'Users\\alice' check() env.pop('HOMEDRIVE', None) env.pop('HOMEPATH', None) env['USERPROFILE'] = 'C:\\Users\\alice' check() # bpo-38883: ignore `HOME` when set on windows env['HOME'] = 'C:\\Users\\eve' check() universal_pathlib-0.3.10/upath/tests/pathlib/test_pathlib_312.py000066400000000000000000004207311514661127100246500ustar00rootroot00000000000000import collections.abc import io import os import sys import errno import pathlib import pickle import socket import stat import tempfile import unittest from contextlib import nullcontext from unittest import mock from ._test_support import import_helper from ._test_support import set_recursion_limit from ._test_support import is_emscripten, is_wasi from . import _test_support as os_helper from ._test_support import TESTFN, FakePath from ..utils import temporary_register try: import grp, pwd except ImportError: grp = pwd = None import upath from upath.core import UPath from upath.implementations.local import PosixUPath, WindowsUPath import pytest pytestmark = pytest.mark.skipif(sys.version_info[:2] != (3, 12), reason="py312 only") # # Tests for the pure classes. # class _BasePurePathSubclass(object): @property def session_id(self): return self.storage_options["session_id"] class _BasePurePathTest(object): # Keys are canonical paths, values are list of tuples of arguments # supposed to produce equal paths. equivalences = { 'a/b': [ ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'), ('a/b/',), ('a//b',), ('a//b//',), # Empty components get removed. ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''), ], '/b/c/d': [ ('a', '/b/c', 'd'), ('/a', '/b/c', 'd'), # Empty components get removed. ('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'), ], } def setUp(self): p = self.cls('a') self.flavour = p._flavour self.sep = self.flavour.sep self.altsep = self.flavour.altsep def test_constructor_common(self): P = self.cls p = P('a') self.assertIsInstance(p, P) P('a', 'b', 'c') P('/a', 'b', 'c') P('a/b/c') P('/a/b/c') P(FakePath("a/b/c")) self.assertEqual(P(P('a')), P('a')) self.assertEqual(P(P('a'), 'b'), P('a/b')) self.assertEqual(P(P('a'), P('b')), P('a/b')) self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c"))) if os.name != "nt": self.assertEqual(P(P('./a:b')), P('./a:b')) def test_bytes(self): P = self.cls with self.assertRaises(TypeError): P(b'a') with self.assertRaises(TypeError): P(b'a', 'b') with self.assertRaises(TypeError): P('a', b'b') with self.assertRaises(TypeError): P('a').joinpath(b'b') with self.assertRaises(TypeError): P('a') / b'b' with self.assertRaises(TypeError): b'a' / P('b') with self.assertRaises(TypeError): P('a').match(b'b') with self.assertRaises(TypeError): P('a').relative_to(b'b') with self.assertRaises(TypeError): P('a').with_name(b'b') with self.assertRaises(TypeError): P('a').with_stem(b'b') with self.assertRaises(TypeError): P('a').with_suffix(b'b') def _check_str_subclass(self, *args): # Issue #21127: it should be possible to construct a PurePath object # from a str subclass instance, and it then gets converted to # a pure str object. class StrSubclass(str): pass P = self.cls p = P(*(StrSubclass(x) for x in args)) self.assertEqual(p, P(*args)) for part in p.parts: self.assertIs(type(part), str) def test_str_subclass_common(self): self._check_str_subclass('') self._check_str_subclass('.') self._check_str_subclass('a') self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') def test_with_segments_common(self): class P(_BasePurePathSubclass, self.cls): pass with temporary_register("", P): p = P('foo', 'bar', session_id=42) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) self.assertEqual(42, p.joinpath('foo').session_id) self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) self.assertEqual(42, p.with_suffix('.foo').session_id) self.assertEqual(42, p.with_segments('foo').session_id) self.assertEqual(42, p.relative_to('foo').session_id) self.assertEqual(42, p.parent.session_id) for parent in p.parents: self.assertEqual(42, parent.session_id) def _get_drive_root_parts(self, parts): path = self.cls(*parts) return path.drive, path.root, path.parts def _check_drive_root_parts(self, arg, *expected): sep = self.flavour.sep actual = self._get_drive_root_parts([x.replace('/', sep) for x in arg]) self.assertEqual(actual, expected) if altsep := self.flavour.altsep: actual = self._get_drive_root_parts([x.replace('/', altsep) for x in arg]) self.assertEqual(actual, expected) def test_drive_root_parts_common(self): check = self._check_drive_root_parts sep = self.flavour.sep # Unanchored parts. check((), '', '', ()) check(('a',), '', '', ('a',)) check(('a/',), '', '', ('a',)) check(('a', 'b'), '', '', ('a', 'b')) # Expansion. check(('a/b',), '', '', ('a', 'b')) check(('a/b/',), '', '', ('a', 'b')) check(('a', 'b/c', 'd'), '', '', ('a', 'b', 'c', 'd')) # Collapsing and stripping excess slashes. check(('a', 'b//c', 'd'), '', '', ('a', 'b', 'c', 'd')) check(('a', 'b/c/', 'd'), '', '', ('a', 'b', 'c', 'd')) # Eliminating standalone dots. check(('.',), '', '', ()) check(('.', '.', 'b'), '', '', ('b',)) check(('a', '.', 'b'), '', '', ('a', 'b')) check(('a', '.', '.'), '', '', ('a',)) # The first part is anchored. check(('/a/b',), '', sep, (sep, 'a', 'b')) check(('/a', 'b'), '', sep, (sep, 'a', 'b')) check(('/a/', 'b'), '', sep, (sep, 'a', 'b')) # Ignoring parts before an anchored part. check(('a', '/b', 'c'), '', sep, (sep, 'b', 'c')) check(('a', '/b', '/c'), '', sep, (sep, 'c')) def test_join_common(self): P = self.cls p = P('a/b') pp = p.joinpath('c') self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p.joinpath('c', 'd') self.assertEqual(pp, P('a/b/c/d')) pp = p.joinpath(P('c')) self.assertEqual(pp, P('a/b/c')) pp = p.joinpath('/c') self.assertEqual(pp, P('/c')) def test_div_common(self): # Basically the same as joinpath(). P = self.cls p = P('a/b') pp = p / 'c' self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p / 'c/d' self.assertEqual(pp, P('a/b/c/d')) pp = p / 'c' / 'd' self.assertEqual(pp, P('a/b/c/d')) pp = 'c' / p / 'd' self.assertEqual(pp, P('c/a/b/d')) pp = p / P('c') self.assertEqual(pp, P('a/b/c')) pp = p/ '/c' self.assertEqual(pp, P('/c')) def _check_str(self, expected, args): p = self.cls(*args) self.assertEqual(str(p), expected.replace('/', self.sep)) def test_str_common(self): # Canonicalized paths roundtrip. for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self._check_str(pathstr, (pathstr,)) # Special case for the empty path. self._check_str('.', ('',)) # Other tests for str() are in test_equivalences(). def test_as_posix_common(self): P = self.cls for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self.assertEqual(P(pathstr).as_posix(), pathstr) # Other tests for as_posix() are in test_equivalences(). def test_as_bytes_common(self): sep = os.fsencode(self.sep) P = self.cls self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b') def test_as_uri_common(self): P = self.cls with self.assertRaises(ValueError): P('a').as_uri() with self.assertRaises(ValueError): P().as_uri() def test_repr_common(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): with self.subTest(pathstr=pathstr): p = self.cls(pathstr) clsname = p.__class__.__name__ r = repr(p) # The repr() is in the form ClassName("forward-slashes path"). self.assertTrue(r.startswith(clsname + '('), r) self.assertTrue(r.endswith(')'), r) inner = r[len(clsname) + 1 : -1] self.assertEqual(eval(inner), p.as_posix()) def test_repr_roundtrips(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): with self.subTest(pathstr=pathstr): p = self.cls(pathstr) r = repr(p) # The repr() roundtrips. q = eval(r, upath.implementations.local.__dict__) self.assertIs(q.__class__, p.__class__) self.assertEqual(q, p) self.assertEqual(repr(q), r) def test_eq_common(self): P = self.cls self.assertEqual(P('a/b'), P('a/b')) self.assertEqual(P('a/b'), P('a', 'b')) self.assertNotEqual(P('a/b'), P('a')) self.assertNotEqual(P('a/b'), P('/a/b')) self.assertNotEqual(P('a/b'), P()) self.assertNotEqual(P('/a/b'), P('/')) self.assertNotEqual(P(), P('/')) self.assertNotEqual(P(), "") self.assertNotEqual(P(), {}) self.assertNotEqual(P(), int) def test_match_common(self): P = self.cls self.assertRaises(ValueError, P('a').match, '') self.assertRaises(ValueError, P('a').match, '.') # Simple relative pattern. self.assertTrue(P('b.py').match('b.py')) self.assertTrue(P('a/b.py').match('b.py')) self.assertTrue(P('/a/b.py').match('b.py')) self.assertFalse(P('a.py').match('b.py')) self.assertFalse(P('b/py').match('b.py')) self.assertFalse(P('/a.py').match('b.py')) self.assertFalse(P('b.py/c').match('b.py')) # Wildcard relative pattern. self.assertTrue(P('b.py').match('*.py')) self.assertTrue(P('a/b.py').match('*.py')) self.assertTrue(P('/a/b.py').match('*.py')) self.assertFalse(P('b.pyc').match('*.py')) self.assertFalse(P('b./py').match('*.py')) self.assertFalse(P('b.py/c').match('*.py')) # Multi-part relative pattern. self.assertTrue(P('ab/c.py').match('a*/*.py')) self.assertTrue(P('/d/ab/c.py').match('a*/*.py')) self.assertFalse(P('a.py').match('a*/*.py')) self.assertFalse(P('/dab/c.py').match('a*/*.py')) self.assertFalse(P('ab/c.py/d').match('a*/*.py')) # Absolute pattern. self.assertTrue(P('/b.py').match('/*.py')) self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('a/b.py').match('/*.py')) self.assertFalse(P('/a/b.py').match('/*.py')) # Multi-part absolute pattern. self.assertTrue(P('/a/b.py').match('/a/*.py')) self.assertFalse(P('/ab.py').match('/a/*.py')) self.assertFalse(P('/a/b/c.py').match('/a/*.py')) # Multi-part glob-style pattern. self.assertFalse(P('/a/b/c.py').match('/**/*.py')) self.assertTrue(P('/a/b/c.py').match('/a/**/*.py')) # Case-sensitive flag self.assertFalse(P('A.py').match('a.PY', case_sensitive=True)) self.assertTrue(P('A.py').match('a.PY', case_sensitive=False)) self.assertFalse(P('c:/a/B.Py').match('C:/A/*.pY', case_sensitive=True)) self.assertTrue(P('/a/b/c.py').match('/A/*/*.Py', case_sensitive=False)) # Matching against empty path self.assertFalse(P().match('*')) self.assertTrue(P().match('**')) self.assertFalse(P().match('**/*')) def test_ordering_common(self): # Ordering is tuple-alike. def assertLess(a, b): self.assertLess(a, b) self.assertGreater(b, a) P = self.cls a = P('a') b = P('a/b') c = P('abc') d = P('b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) P = self.cls a = P('/a') b = P('/a/b') c = P('/abc') d = P('/b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) with self.assertRaises(TypeError): P() < {} def test_parts_common(self): # `parts` returns a tuple. sep = self.sep P = self.cls p = P('a/b') parts = p.parts self.assertEqual(parts, ('a', 'b')) # When the path is absolute, the anchor is a separate part. p = P('/a/b') parts = p.parts self.assertEqual(parts, (sep, 'a', 'b')) def test_fspath_common(self): P = self.cls p = P('a/b') self._check_str(p.__fspath__(), ('a/b',)) self._check_str(os.fspath(p), ('a/b',)) def test_equivalences(self): for k, tuples in self.equivalences.items(): canon = k.replace('/', self.sep) posix = k.replace(self.sep, '/') if canon != posix: tuples = tuples + [ tuple(part.replace('/', self.sep) for part in t) for t in tuples ] tuples.append((posix, )) pcanon = self.cls(canon) for t in tuples: p = self.cls(*t) self.assertEqual(p, pcanon, "failed with args {}".format(t)) self.assertEqual(hash(p), hash(pcanon)) self.assertEqual(str(p), canon) self.assertEqual(p.as_posix(), posix) def test_parent_common(self): # Relative P = self.cls p = P('a/b/c') self.assertEqual(p.parent, P('a/b')) self.assertEqual(p.parent.parent, P('a')) self.assertEqual(p.parent.parent.parent, P()) self.assertEqual(p.parent.parent.parent.parent, P()) # Anchored p = P('/a/b/c') self.assertEqual(p.parent, P('/a/b')) self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative P = self.cls p = P('a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('a/b')) self.assertEqual(par[1], P('a')) self.assertEqual(par[2], P('.')) self.assertEqual(par[-1], P('.')) self.assertEqual(par[-2], P('a')) self.assertEqual(par[-3], P('a/b')) self.assertEqual(par[0:1], (P('a/b'),)) self.assertEqual(par[:2], (P('a/b'), P('a'))) self.assertEqual(par[:-1], (P('a/b'), P('a'))) self.assertEqual(par[1:], (P('a'), P('.'))) self.assertEqual(par[::2], (P('a/b'), P('.'))) self.assertEqual(par[::-1], (P('.'), P('a'), P('a/b'))) self.assertEqual(list(par), [P('a/b'), P('a'), P('.')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] with self.assertRaises(TypeError): par[0] = p # Anchored p = P('/a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('/a/b')) self.assertEqual(par[1], P('/a')) self.assertEqual(par[2], P('/')) self.assertEqual(par[-1], P('/')) self.assertEqual(par[-2], P('/a')) self.assertEqual(par[-3], P('/a/b')) self.assertEqual(par[0:1], (P('/a/b'),)) self.assertEqual(par[:2], (P('/a/b'), P('/a'))) self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) self.assertEqual(par[1:], (P('/a'), P('/'))) self.assertEqual(par[::2], (P('/a/b'), P('/'))) self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] def test_drive_common(self): P = self.cls self.assertEqual(P('a/b').drive, '') self.assertEqual(P('/a/b').drive, '') self.assertEqual(P('').drive, '') def test_root_common(self): P = self.cls sep = self.sep self.assertEqual(P('').root, '') self.assertEqual(P('a/b').root, '') self.assertEqual(P('/').root, sep) self.assertEqual(P('/a/b').root, sep) def test_anchor_common(self): P = self.cls sep = self.sep self.assertEqual(P('').anchor, '') self.assertEqual(P('a/b').anchor, '') self.assertEqual(P('/').anchor, sep) self.assertEqual(P('/a/b').anchor, sep) def test_name_common(self): P = self.cls self.assertEqual(P('').name, '') self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') self.assertEqual(P('/a/b').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') def test_suffix_common(self): P = self.cls self.assertEqual(P('').suffix, '') self.assertEqual(P('.').suffix, '') self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') self.assertEqual(P('/a/b').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') self.assertEqual(P('a/.hgrc').suffix, '') self.assertEqual(P('/a/.hgrc').suffix, '') self.assertEqual(P('a/.hg.rc').suffix, '.rc') self.assertEqual(P('/a/.hg.rc').suffix, '.rc') self.assertEqual(P('a/b.tar.gz').suffix, '.gz') self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') def test_suffixes_common(self): P = self.cls self.assertEqual(P('').suffixes, []) self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) self.assertEqual(P('a/.hgrc').suffixes, []) self.assertEqual(P('/a/.hgrc').suffixes, []) self.assertEqual(P('a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) def test_stem_common(self): P = self.cls self.assertEqual(P('').stem, '') self.assertEqual(P('.').stem, '') self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name_common(self): P = self.cls self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') self.assertRaises(ValueError, P('a/b').with_name, '') # self.assertRaises(ValueError, P('a/b').with_name, '.') self.assertRaises(ValueError, P('a/b').with_name, '/c') self.assertRaises(ValueError, P('a/b').with_name, 'c/') self.assertRaises(ValueError, P('a/b').with_name, 'c/d') def test_with_stem_common(self): P = self.cls self.assertEqual(P('a/b').with_stem('d'), P('a/d')) self.assertEqual(P('/a/b').with_stem('d'), P('/a/d')) self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py')) self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py')) self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') self.assertRaises(ValueError, P('a/b').with_stem, '') # self.assertRaises(ValueError, P('a/b').with_stem, '.') self.assertRaises(ValueError, P('a/b').with_stem, '/c') self.assertRaises(ValueError, P('a/b').with_stem, 'c/') self.assertRaises(ValueError, P('a/b').with_stem, 'c/d') def test_with_suffix_common(self): P = self.cls self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('a/b').with_suffix, '/') self.assertRaises(ValueError, P('a/b').with_suffix, '.') self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d') self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(ValueError, P('a/b').with_suffix, (self.flavour.sep, 'd')) def test_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.relative_to) self.assertRaises(TypeError, p.relative_to, b'a') self.assertEqual(p.relative_to(P()), P('a/b')) self.assertEqual(p.relative_to(''), P('a/b')) self.assertEqual(p.relative_to(P('a')), P('b')) self.assertEqual(p.relative_to('a'), P('b')) self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to('a/b'), P()) self.assertEqual(p.relative_to(P(), walk_up=True), P('a/b')) self.assertEqual(p.relative_to('', walk_up=True), P('a/b')) self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b')) self.assertEqual(p.relative_to('a', walk_up=True), P('b')) self.assertEqual(p.relative_to('a/', walk_up=True), P('b')) self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P()) self.assertEqual(p.relative_to('a/b', walk_up=True), P()) self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('../b')) self.assertEqual(p.relative_to('a/c', walk_up=True), P('../b')) self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..')) self.assertEqual(p.relative_to('a/b/c', walk_up=True), P('..')) self.assertEqual(p.relative_to(P('c'), walk_up=True), P('../a/b')) self.assertEqual(p.relative_to('c', walk_up=True), P('../a/b')) # With several args. with self.assertWarns(DeprecationWarning): p.relative_to('a', 'b') p.relative_to('a', 'b', walk_up=True) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('c')) self.assertRaises(ValueError, p.relative_to, P('a/b/c')) self.assertRaises(ValueError, p.relative_to, P('a/c')) self.assertRaises(ValueError, p.relative_to, P('/a')) self.assertRaises(ValueError, p.relative_to, P("../a")) self.assertRaises(ValueError, p.relative_to, P("a/..")) self.assertRaises(ValueError, p.relative_to, P("/a/..")) self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("../a"), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("a/.."), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("/a/.."), walk_up=True) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) self.assertEqual(p.relative_to(P('/a')), P('b')) self.assertEqual(p.relative_to('/a'), P('b')) self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to('/a/b'), P()) self.assertEqual(p.relative_to(P('/'), walk_up=True), P('a/b')) self.assertEqual(p.relative_to('/', walk_up=True), P('a/b')) self.assertEqual(p.relative_to(P('/a'), walk_up=True), P('b')) self.assertEqual(p.relative_to('/a', walk_up=True), P('b')) self.assertEqual(p.relative_to('/a/', walk_up=True), P('b')) self.assertEqual(p.relative_to(P('/a/b'), walk_up=True), P()) self.assertEqual(p.relative_to('/a/b', walk_up=True), P()) self.assertEqual(p.relative_to(P('/a/c'), walk_up=True), P('../b')) self.assertEqual(p.relative_to('/a/c', walk_up=True), P('../b')) self.assertEqual(p.relative_to(P('/a/b/c'), walk_up=True), P('..')) self.assertEqual(p.relative_to('/a/b/c', walk_up=True), P('..')) self.assertEqual(p.relative_to(P('/c'), walk_up=True), P('../a/b')) self.assertEqual(p.relative_to('/c', walk_up=True), P('../a/b')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) self.assertRaises(ValueError, p.relative_to, P('/a/c')) self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('a')) self.assertRaises(ValueError, p.relative_to, P("../a")) self.assertRaises(ValueError, p.relative_to, P("a/..")) self.assertRaises(ValueError, p.relative_to, P("/a/..")) self.assertRaises(ValueError, p.relative_to, P(''), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('a'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("../a"), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("a/.."), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("/a/.."), walk_up=True) def test_is_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.is_relative_to) self.assertRaises(TypeError, p.is_relative_to, b'a') self.assertTrue(p.is_relative_to(P())) self.assertTrue(p.is_relative_to('')) self.assertTrue(p.is_relative_to(P('a'))) self.assertTrue(p.is_relative_to('a/')) self.assertTrue(p.is_relative_to(P('a/b'))) self.assertTrue(p.is_relative_to('a/b')) # With several args. with self.assertWarns(DeprecationWarning): p.is_relative_to('a', 'b') # Unrelated paths. self.assertFalse(p.is_relative_to(P('c'))) self.assertFalse(p.is_relative_to(P('a/b/c'))) self.assertFalse(p.is_relative_to(P('a/c'))) self.assertFalse(p.is_relative_to(P('/a'))) p = P('/a/b') self.assertTrue(p.is_relative_to(P('/'))) self.assertTrue(p.is_relative_to('/')) self.assertTrue(p.is_relative_to(P('/a'))) self.assertTrue(p.is_relative_to('/a')) self.assertTrue(p.is_relative_to('/a/')) self.assertTrue(p.is_relative_to(P('/a/b'))) self.assertTrue(p.is_relative_to('/a/b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/c'))) self.assertFalse(p.is_relative_to(P('/a/b/c'))) self.assertFalse(p.is_relative_to(P('/a/c'))) self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) def test_pickling_common(self): P = self.cls p = P('/a/b') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertIs(pp.__class__, p.__class__) self.assertEqual(pp, p) self.assertEqual(hash(pp), hash(p)) self.assertEqual(str(pp), str(p)) class PurePosixPathTest(_BasePurePathTest): cls = pathlib.PurePosixPath def test_drive_root_parts(self): check = self._check_drive_root_parts # Collapsing of excess leading slashes, except for the double-slash # special case. check(('//a', 'b'), '', '//', ('//', 'a', 'b')) check(('///a', 'b'), '', '/', ('/', 'a', 'b')) check(('////a', 'b'), '', '/', ('/', 'a', 'b')) # Paths which look like NT paths aren't treated specially. check(('c:a',), '', '', ('c:a',)) check(('c:\\a',), '', '', ('c:\\a',)) check(('\\a',), '', '', ('\\a',)) def test_root(self): P = self.cls self.assertEqual(P('/a/b').root, '/') self.assertEqual(P('///a/b').root, '/') # POSIX special case for two leading slashes. self.assertEqual(P('//a/b').root, '//') def test_eq(self): P = self.cls self.assertNotEqual(P('a/b'), P('A/b')) self.assertEqual(P('/a'), P('///a')) self.assertNotEqual(P('/a'), P('//a')) def test_as_uri(self): P = self.cls self.assertEqual(P('/').as_uri(), 'file:///') self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c') self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c') def test_as_uri_non_ascii(self): from urllib.parse import quote_from_bytes P = self.cls try: os.fsencode('\xe9') except UnicodeEncodeError: self.skipTest("\\xe9 cannot be encoded to the filesystem encoding") self.assertEqual(P('/a/b\xe9').as_uri(), 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9'))) def test_match(self): P = self.cls self.assertFalse(P('A.py').match('a.PY')) def test_is_absolute(self): P = self.cls self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertTrue(P('/').is_absolute()) self.assertTrue(P('/a').is_absolute()) self.assertTrue(P('/a/b/').is_absolute()) self.assertTrue(P('//a').is_absolute()) self.assertTrue(P('//a/b').is_absolute()) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved()) def test_join(self): P = self.cls p = P('//a') pp = p.joinpath('b') self.assertEqual(pp, P('//a/b')) pp = P('/a').joinpath('//c') self.assertEqual(pp, P('//c')) pp = P('//a').joinpath('/c') self.assertEqual(pp, P('/c')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) def test_parse_windows_path(self): P = self.cls p = P('c:', 'a', 'b') pp = P(pathlib.PureWindowsPath('c:\\a\\b')) self.assertEqual(p, pp) class PureWindowsPathTest(_BasePurePathTest): cls = pathlib.PureWindowsPath equivalences = _BasePurePathTest.equivalences.copy() equivalences.update({ './a:b': [ ('./a:b',) ], 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('.', 'c:', 'a') ], 'c:/a': [ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'), ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'), ], '//a/b/': [ ('//a/b',) ], '//a/b/c': [ ('//a/b', 'c'), ('//a/b/', 'c'), ], }) def test_drive_root_parts(self): check = self._check_drive_root_parts # First part is anchored. check(('c:',), 'c:', '', ('c:',)) check(('c:/',), 'c:', '\\', ('c:\\',)) check(('/',), '', '\\', ('\\',)) check(('c:a',), 'c:', '', ('c:', 'a')) check(('c:/a',), 'c:', '\\', ('c:\\', 'a')) check(('/a',), '', '\\', ('\\', 'a')) # UNC paths. check(('//',), '\\\\', '', ('\\\\',)) check(('//a',), '\\\\a', '', ('\\\\a',)) check(('//a/',), '\\\\a\\', '', ('\\\\a\\',)) check(('//a/b',), '\\\\a\\b', '\\', ('\\\\a\\b\\',)) check(('//a/b/',), '\\\\a\\b', '\\', ('\\\\a\\b\\',)) check(('//a/b/c',), '\\\\a\\b', '\\', ('\\\\a\\b\\', 'c')) # Second part is anchored, so that the first part is ignored. check(('a', 'Z:b', 'c'), 'Z:', '', ('Z:', 'b', 'c')) check(('a', 'Z:/b', 'c'), 'Z:', '\\', ('Z:\\', 'b', 'c')) # UNC paths. check(('a', '//b/c', 'd'), '\\\\b\\c', '\\', ('\\\\b\\c\\', 'd')) # Collapsing and stripping excess slashes. check(('a', 'Z://b//c/', 'd/'), 'Z:', '\\', ('Z:\\', 'b', 'c', 'd')) # UNC paths. check(('a', '//b/c//', 'd'), '\\\\b\\c', '\\', ('\\\\b\\c\\', 'd')) # Extended paths. check(('//./c:',), '\\\\.\\c:', '', ('\\\\.\\c:',)) check(('//?/c:/',), '\\\\?\\c:', '\\', ('\\\\?\\c:\\',)) check(('//?/c:/a',), '\\\\?\\c:', '\\', ('\\\\?\\c:\\', 'a')) check(('//?/c:/a', '/b'), '\\\\?\\c:', '\\', ('\\\\?\\c:\\', 'b')) # Extended UNC paths (format is "\\?\UNC\server\share"). check(('//?',), '\\\\?', '', ('\\\\?',)) check(('//?/',), '\\\\?\\', '', ('\\\\?\\',)) check(('//?/UNC',), '\\\\?\\UNC', '', ('\\\\?\\UNC',)) check(('//?/UNC/',), '\\\\?\\UNC\\', '', ('\\\\?\\UNC\\',)) check(('//?/UNC/b',), '\\\\?\\UNC\\b', '', ('\\\\?\\UNC\\b',)) check(('//?/UNC/b/',), '\\\\?\\UNC\\b\\', '', ('\\\\?\\UNC\\b\\',)) check(('//?/UNC/b/c',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\',)) check(('//?/UNC/b/c/',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\',)) check(('//?/UNC/b/c/d',), '\\\\?\\UNC\\b\\c', '\\', ('\\\\?\\UNC\\b\\c\\', 'd')) # UNC device paths check(('//./BootPartition/',), '\\\\.\\BootPartition', '\\', ('\\\\.\\BootPartition\\',)) check(('//?/BootPartition/',), '\\\\?\\BootPartition', '\\', ('\\\\?\\BootPartition\\',)) check(('//./PhysicalDrive0',), '\\\\.\\PhysicalDrive0', '', ('\\\\.\\PhysicalDrive0',)) check(('//?/Volume{}/',), '\\\\?\\Volume{}', '\\', ('\\\\?\\Volume{}\\',)) check(('//./nul',), '\\\\.\\nul', '', ('\\\\.\\nul',)) # Second part has a root but not drive. check(('a', '/b', 'c'), '', '\\', ('\\', 'b', 'c')) check(('Z:/a', '/b', 'c'), 'Z:', '\\', ('Z:\\', 'b', 'c')) check(('//?/Z:/a', '/b', 'c'), '\\\\?\\Z:', '\\', ('\\\\?\\Z:\\', 'b', 'c')) # Joining with the same drive => the first path is appended to if # the second path is relative. check(('c:/a/b', 'c:x/y'), 'c:', '\\', ('c:\\', 'a', 'b', 'x', 'y')) check(('c:/a/b', 'c:/x/y'), 'c:', '\\', ('c:\\', 'x', 'y')) # Paths to files with NTFS alternate data streams check(('./c:s',), '', '', ('c:s',)) check(('cc:s',), '', '', ('cc:s',)) check(('C:c:s',), 'C:', '', ('C:', 'c:s')) check(('C:/c:s',), 'C:', '\\', ('C:\\', 'c:s')) check(('D:a', './c:b'), 'D:', '', ('D:', 'a', 'c:b')) check(('D:/a', './c:b'), 'D:', '\\', ('D:\\', 'a', 'c:b')) def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') p = self.cls('c:/a/b/c') self.assertEqual(str(p), 'c:\\a\\b\\c') p = self.cls('//a/b') self.assertEqual(str(p), '\\\\a\\b\\') p = self.cls('//a/b/c') self.assertEqual(str(p), '\\\\a\\b\\c') p = self.cls('//a/b/c/d') self.assertEqual(str(p), '\\\\a\\b\\c\\d') def test_str_subclass(self): self._check_str_subclass('.\\a:b') self._check_str_subclass('c:') self._check_str_subclass('c:a') self._check_str_subclass('c:a\\b.txt') self._check_str_subclass('c:\\') self._check_str_subclass('c:\\a') self._check_str_subclass('c:\\a\\b.txt') self._check_str_subclass('\\\\some\\share') self._check_str_subclass('\\\\some\\share\\a') self._check_str_subclass('\\\\some\\share\\a\\b.txt') def test_eq(self): P = self.cls self.assertEqual(P('c:a/b'), P('c:a/b')) self.assertEqual(P('c:a/b'), P('c:', 'a', 'b')) self.assertNotEqual(P('c:a/b'), P('d:a/b')) self.assertNotEqual(P('c:a/b'), P('c:/a/b')) self.assertNotEqual(P('/a/b'), P('c:/a/b')) # Case-insensitivity. self.assertEqual(P('a/B'), P('A/b')) self.assertEqual(P('C:a/B'), P('c:A/b')) self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b')) self.assertEqual(P('\u0130'), P('i\u0307')) def test_as_uri(self): P = self.cls with self.assertRaises(ValueError): P('/a/b').as_uri() with self.assertRaises(ValueError): P('c:a/b').as_uri() self.assertEqual(P('c:/').as_uri(), 'file:///c:/') self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c') self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c') self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9') self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/') self.assertEqual(P('//some/share/a/b.c').as_uri(), 'file://some/share/a/b.c') self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(), 'file://some/share/a/b%25%23c%C3%A9') def test_match(self): P = self.cls # Absolute patterns. self.assertTrue(P('c:/b.py').match('*:/*.py')) self.assertTrue(P('c:/b.py').match('c:/*.py')) self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('b.py').match('c:*.py')) self.assertFalse(P('b.py').match('c:/*.py')) self.assertFalse(P('c:b.py').match('/*.py')) self.assertFalse(P('c:b.py').match('c:/*.py')) self.assertFalse(P('/b.py').match('c:*.py')) self.assertFalse(P('/b.py').match('c:/*.py')) # UNC patterns. self.assertTrue(P('//some/share/a.py').match('//*/*/*.py')) self.assertTrue(P('//some/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//other/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py')) # Case-insensitivity. self.assertTrue(P('B.py').match('b.PY')) self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY')) self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY')) # Path anchor doesn't match pattern anchor self.assertFalse(P('c:/b.py').match('/*.py')) # 'c:/' vs '/' self.assertFalse(P('c:/b.py').match('c:*.py')) # 'c:/' vs 'c:' self.assertFalse(P('//some/share/a.py').match('/*.py')) # '//some/share/' vs '/' def test_ordering_common(self): # Case-insensitivity. def assertOrderedEqual(a, b): self.assertLessEqual(a, b) self.assertGreaterEqual(b, a) P = self.cls p = P('c:A/b') q = P('C:a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) p = P('//some/Share/A/b') q = P('//Some/SHARE/a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) def test_parts(self): P = self.cls p = P('c:a/b') parts = p.parts self.assertEqual(parts, ('c:', 'a', 'b')) p = P('c:/a/b') parts = p.parts self.assertEqual(parts, ('c:\\', 'a', 'b')) p = P('//a/b/c/d') parts = p.parts self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd')) def test_parent(self): # Anchored P = self.cls p = P('z:a/b/c') self.assertEqual(p.parent, P('z:a/b')) self.assertEqual(p.parent.parent, P('z:a')) self.assertEqual(p.parent.parent.parent, P('z:')) self.assertEqual(p.parent.parent.parent.parent, P('z:')) p = P('z:/a/b/c') self.assertEqual(p.parent, P('z:/a/b')) self.assertEqual(p.parent.parent, P('z:/a')) self.assertEqual(p.parent.parent.parent, P('z:/')) self.assertEqual(p.parent.parent.parent.parent, P('z:/')) p = P('//a/b/c/d') self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) def test_parents(self): # Anchored P = self.cls p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:a')) self.assertEqual(par[1], P('z:')) self.assertEqual(par[0:1], (P('z:a'),)) self.assertEqual(par[:-1], (P('z:a'),)) self.assertEqual(par[:2], (P('z:a'), P('z:'))) self.assertEqual(par[1:], (P('z:'),)) self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:/a')) self.assertEqual(par[1], P('z:/')) self.assertEqual(par[0:1], (P('z:/a'),)) self.assertEqual(par[0:-1], (P('z:/a'),)) self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) self.assertEqual(par[1:], (P('z:/'),)) self.assertEqual(par[::2], (P('z:/a'),)) self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('//a/b/c/d') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('//a/b/c')) self.assertEqual(par[1], P('//a/b')) self.assertEqual(par[0:1], (P('//a/b/c'),)) self.assertEqual(par[0:-1], (P('//a/b/c'),)) self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b'))) self.assertEqual(par[1:], (P('//a/b'),)) self.assertEqual(par[::2], (P('//a/b/c'),)) self.assertEqual(par[::-1], (P('//a/b'), P('//a/b/c'))) self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')]) with self.assertRaises(IndexError): par[2] def test_drive(self): P = self.cls self.assertEqual(P('c:').drive, 'c:') self.assertEqual(P('c:a/b').drive, 'c:') self.assertEqual(P('c:/').drive, 'c:') self.assertEqual(P('c:/a/b/').drive, 'c:') self.assertEqual(P('//a/b').drive, '\\\\a\\b') self.assertEqual(P('//a/b/').drive, '\\\\a\\b') self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b') self.assertEqual(P('./c:a').drive, '') def test_root(self): P = self.cls self.assertEqual(P('c:').root, '') self.assertEqual(P('c:a/b').root, '') self.assertEqual(P('c:/').root, '\\') self.assertEqual(P('c:/a/b/').root, '\\') self.assertEqual(P('//a/b').root, '\\') self.assertEqual(P('//a/b/').root, '\\') self.assertEqual(P('//a/b/c/d').root, '\\') def test_anchor(self): P = self.cls self.assertEqual(P('c:').anchor, 'c:') self.assertEqual(P('c:a/b').anchor, 'c:') self.assertEqual(P('c:/').anchor, 'c:\\') self.assertEqual(P('c:/a/b/').anchor, 'c:\\') self.assertEqual(P('//a/b').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/c/d').anchor, '\\\\a\\b\\') def test_name(self): P = self.cls self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') self.assertEqual(P('c:/a/.hgrc').suffix, '') self.assertEqual(P('c:a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) self.assertEqual(P('c:/a/.hgrc').suffixes, []) self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('//My.py/Share.php').suffixes, []) self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) def test_stem(self): P = self.cls self.assertEqual(P('c:').stem, '') self.assertEqual(P('c:.').stem, '') self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name(self): P = self.cls self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') # self.assertRaises(ValueError, P('c:a/b').with_name, 'd:') # self.assertRaises(ValueError, P('c:a/b').with_name, 'd:e') # self.assertRaises(ValueError, P('c:a/b').with_name, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_name, '//My/Share') def test_with_stem(self): P = self.cls self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') # self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:') # self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:e') # self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share') def test_with_suffix(self): P = self.cls self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d') def test_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:foO')), P('Bar')) self.assertEqual(p.relative_to('c:foO'), P('Bar')) self.assertEqual(p.relative_to('c:foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:foO/baR')), P()) self.assertEqual(p.relative_to('c:foO/baR'), P()) self.assertEqual(p.relative_to(P('c:'), walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to('c:', walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:foO'), walk_up=True), P('Bar')) self.assertEqual(p.relative_to('c:foO', walk_up=True), P('Bar')) self.assertEqual(p.relative_to('c:foO/', walk_up=True), P('Bar')) self.assertEqual(p.relative_to(P('c:foO/baR'), walk_up=True), P()) self.assertEqual(p.relative_to('c:foO/baR', walk_up=True), P()) self.assertEqual(p.relative_to(P('C:Foo/Bar/Baz'), walk_up=True), P('..')) self.assertEqual(p.relative_to(P('C:Foo/Baz'), walk_up=True), P('../Bar')) self.assertEqual(p.relative_to(P('C:Baz/Bar'), walk_up=True), P('../../Foo/Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('Foo')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P(), walk_up=True) self.assertRaises(ValueError, p.relative_to, '', walk_up=True) self.assertRaises(ValueError, p.relative_to, P('d:'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('C:/Foo'), walk_up=True) p = P('C:/Foo/Bar') self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:/foO')), P('Bar')) self.assertEqual(p.relative_to('c:/foO'), P('Bar')) self.assertEqual(p.relative_to('c:/foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:/foO/baR')), P()) self.assertEqual(p.relative_to('c:/foO/baR'), P()) self.assertEqual(p.relative_to(P('c:/'), walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to('c:/', walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:/foO'), walk_up=True), P('Bar')) self.assertEqual(p.relative_to('c:/foO', walk_up=True), P('Bar')) self.assertEqual(p.relative_to('c:/foO/', walk_up=True), P('Bar')) self.assertEqual(p.relative_to(P('c:/foO/baR'), walk_up=True), P()) self.assertEqual(p.relative_to('c:/foO/baR', walk_up=True), P()) self.assertEqual(p.relative_to('C:/Baz', walk_up=True), P('../Foo/Bar')) self.assertEqual(p.relative_to('C:/Foo/Bar/Baz', walk_up=True), P('..')) self.assertEqual(p.relative_to('C:/Foo/Baz', walk_up=True), P('../Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, 'c:') self.assertRaises(ValueError, p.relative_to, P('c:')) self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo')) self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('d:/')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('//C/Foo')) self.assertRaises(ValueError, p.relative_to, 'c:', walk_up=True) self.assertRaises(ValueError, p.relative_to, P('c:'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('C:Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('d:'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('d:/'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('//C/Foo'), walk_up=True) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare'), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo')), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo'), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) self.assertEqual(p.relative_to(P('//sErver/sHare'), walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare', walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/', walk_up=True), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo'), walk_up=True), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo', walk_up=True), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo/', walk_up=True), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar'), walk_up=True), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar', walk_up=True), P()) self.assertEqual(p.relative_to(P('//sErver/sHare/bar'), walk_up=True), P('../Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/bar', walk_up=True), P('../Foo/Bar')) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo')) self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo'), walk_up=True) def test_is_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertTrue(p.is_relative_to(P('c:'))) self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:foO'))) self.assertTrue(p.is_relative_to('c:foO')) self.assertTrue(p.is_relative_to('c:foO/')) self.assertTrue(p.is_relative_to(P('c:foO/baR'))) self.assertTrue(p.is_relative_to('c:foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('Foo'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('C:/Foo'))) self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo/Baz'))) p = P('C:/Foo/Bar') self.assertTrue(p.is_relative_to(P('c:/'))) self.assertTrue(p.is_relative_to(P('c:/foO'))) self.assertTrue(p.is_relative_to('c:/foO/')) self.assertTrue(p.is_relative_to(P('c:/foO/baR'))) self.assertTrue(p.is_relative_to('c:/foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to('c:')) self.assertFalse(p.is_relative_to(P('C:/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo'))) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('d:/'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('//C/Foo'))) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertTrue(p.is_relative_to(P('//sErver/sHare'))) self.assertTrue(p.is_relative_to('//sErver/sHare')) self.assertTrue(p.is_relative_to('//sErver/sHare/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo')) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo/Bar'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/Bar')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('c:/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//z/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//Server/z/Foo'))) def test_is_absolute(self): P = self.cls # Under NT, only paths with both a drive and a root are absolute. self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertFalse(P('/').is_absolute()) self.assertFalse(P('/a').is_absolute()) self.assertFalse(P('/a/b/').is_absolute()) self.assertFalse(P('c:').is_absolute()) self.assertFalse(P('c:a').is_absolute()) self.assertFalse(P('c:a/b/').is_absolute()) self.assertTrue(P('c:/').is_absolute()) self.assertTrue(P('c:/a').is_absolute()) self.assertTrue(P('c:/a/b/').is_absolute()) # UNC paths are absolute by definition. self.assertTrue(P('//a/b').is_absolute()) self.assertTrue(P('//a/b/').is_absolute()) self.assertTrue(P('//a/b/c').is_absolute()) self.assertTrue(P('//a/b/c/d').is_absolute()) def test_join(self): P = self.cls p = P('C:/a/b') pp = p.joinpath('x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. pp = p.joinpath('D:x/y') self.assertEqual(pp, P('D:x/y')) pp = p.joinpath('D:/x/y') self.assertEqual(pp, P('D:/x/y')) pp = p.joinpath('//host/share/x/y') self.assertEqual(pp, P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. pp = p.joinpath('c:x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('c:/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with files with NTFS data streams => the filename should # not be parsed as a drive letter pp = p.joinpath(P('./d:s')) self.assertEqual(pp, P('C:/a/b/d:s')) pp = p.joinpath(P('./dd:s')) self.assertEqual(pp, P('C:/a/b/dd:s')) pp = p.joinpath(P('E:d:s')) self.assertEqual(pp, P('E:d:s')) # Joining onto a UNC path with no root pp = P('//').joinpath('server') self.assertEqual(pp, P('//server')) pp = P('//server').joinpath('share') self.assertEqual(pp, P('//server/share')) pp = P('//./BootPartition').joinpath('Windows') self.assertEqual(pp, P('//./BootPartition/Windows')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('C:/a/b') self.assertEqual(p / 'x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'x' / 'y', P('C:/a/b/x/y')) self.assertEqual(p / '/x/y', P('C:/x/y')) self.assertEqual(p / '/x' / 'y', P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. self.assertEqual(p / 'D:x/y', P('D:x/y')) self.assertEqual(p / 'D:' / 'x/y', P('D:x/y')) self.assertEqual(p / 'D:/x/y', P('D:/x/y')) self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y')) self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'c:/x/y', P('C:/x/y')) # Joining with files with NTFS data streams => the filename should # not be parsed as a drive letter self.assertEqual(p / P('./d:s'), P('C:/a/b/d:s')) self.assertEqual(p / P('./dd:s'), P('C:/a/b/dd:s')) self.assertEqual(p / P('E:d:s'), P('E:d:s')) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) # UNC paths are never reserved. self.assertIs(False, P('//my/share/nul/con/aux').is_reserved()) # Case-insensitive DOS-device names are reserved. self.assertIs(True, P('nul').is_reserved()) self.assertIs(True, P('aux').is_reserved()) self.assertIs(True, P('prn').is_reserved()) self.assertIs(True, P('con').is_reserved()) self.assertIs(True, P('conin$').is_reserved()) self.assertIs(True, P('conout$').is_reserved()) # COM/LPT + 1-9 or + superscript 1-3 are reserved. self.assertIs(True, P('COM1').is_reserved()) self.assertIs(True, P('LPT9').is_reserved()) self.assertIs(True, P('com\xb9').is_reserved()) self.assertIs(True, P('com\xb2').is_reserved()) self.assertIs(True, P('lpt\xb3').is_reserved()) # DOS-device name mataching ignores characters after a dot or # a colon and also ignores trailing spaces. self.assertIs(True, P('NUL.txt').is_reserved()) self.assertIs(True, P('PRN ').is_reserved()) self.assertIs(True, P('AUX .txt').is_reserved()) self.assertIs(True, P('COM1:bar').is_reserved()) self.assertIs(True, P('LPT9 :bar').is_reserved()) # DOS-device names are only matched at the beginning # of a path component. self.assertIs(False, P('bar.com9').is_reserved()) self.assertIs(False, P('bar.lpt9').is_reserved()) # Only the last path component matters. self.assertIs(True, P('c:/baz/con/NUL').is_reserved()) self.assertIs(False, P('c:/NUL/con/baz').is_reserved()) class PurePathTest(_BasePurePathTest): cls = pathlib.PurePath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath) def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') self.assertNotEqual(p, q) def test_different_flavours_unordered(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') with self.assertRaises(TypeError): p < q with self.assertRaises(TypeError): p <= q with self.assertRaises(TypeError): p > q with self.assertRaises(TypeError): p >= q # # Tests for the concrete classes. # # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) rel_join = lambda *x: os.path.join(TESTFN, *x) only_nt = unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system') only_posix = unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system') @only_posix class PosixPathAsPureTest(PurePosixPathTest, unittest.TestCase): cls = PosixUPath @only_nt class WindowsPathAsPureTest(PureWindowsPathTest, unittest.TestCase): cls = WindowsUPath def test_owner(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').owner() def test_group(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').group() class _BasePathTest(object): """Tests for the FS-accessing functionalities of the Path classes.""" # (BASE) # | # |-- brokenLink -> non-existing # |-- dirA # | `-- linkC -> ../dirB # |-- dirB # | |-- fileB # | `-- linkD -> ../dirB # |-- dirC # | |-- dirD # | | `-- fileD # | `-- fileC # | `-- novel.txt # |-- dirE # No permissions # |-- fileA # |-- linkA -> fileA # |-- linkB -> dirB # `-- brokenLinkLoop -> brokenLinkLoop # def setUp(self): def cleanup(): os.chmod(join('dirE'), 0o777) os_helper.rmtree(BASE) self.addCleanup(cleanup) os.mkdir(BASE) os.mkdir(join('dirA')) os.mkdir(join('dirB')) os.mkdir(join('dirC')) os.mkdir(join('dirC', 'dirD')) os.mkdir(join('dirE')) with open(join('fileA'), 'wb') as f: f.write(b"this is file A\n") with open(join('dirB', 'fileB'), 'wb') as f: f.write(b"this is file B\n") with open(join('dirC', 'fileC'), 'wb') as f: f.write(b"this is file C\n") with open(join('dirC', 'novel.txt'), 'wb') as f: f.write(b"this is a novel\n") with open(join('dirC', 'dirD', 'fileD'), 'wb') as f: f.write(b"this is file D\n") os.chmod(join('dirE'), 0) if os_helper.can_symlink(): # Relative symlinks. os.symlink('fileA', join('linkA')) os.symlink('non-existing', join('brokenLink')) self.dirlink('dirB', join('linkB')) self.dirlink(os.path.join('..', 'dirB'), join('dirA', 'linkC')) # This one goes upwards, creating a loop. self.dirlink(os.path.join('..', 'dirB'), join('dirB', 'linkD')) # Broken symlink (pointing to itself). os.symlink('brokenLinkLoop', join('brokenLinkLoop')) if os.name == 'nt': # Workaround for http://bugs.python.org/issue13772. def dirlink(self, src, dest): os.symlink(src, dest, target_is_directory=True) else: def dirlink(self, src, dest): os.symlink(src, dest) def assertSame(self, path_a, path_b): self.assertTrue(os.path.samefile(str(path_a), str(path_b)), "%r and %r don't point to the same file" % (path_a, path_b)) def assertFileNotFound(self, func, *args, **kwargs): with self.assertRaises(FileNotFoundError) as cm: func(*args, **kwargs) self.assertEqual(cm.exception.errno, errno.ENOENT) def assertEqualNormCase(self, path_a, path_b): self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b)) def _test_cwd(self, p): q = self.cls(os.getcwd()) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_cwd(self): p = self.cls.cwd() self._test_cwd(p) def test_absolute_common(self): P = self.cls with mock.patch("os.getcwd") as getcwd: getcwd.return_value = BASE # Simple relative paths. self.assertEqual(str(P().absolute()), BASE) self.assertEqual(str(P('.').absolute()), BASE) self.assertEqual(str(P('a').absolute()), os.path.join(BASE, 'a')) self.assertEqual(str(P('a', 'b', 'c').absolute()), os.path.join(BASE, 'a', 'b', 'c')) # Symlinks should not be resolved. self.assertEqual(str(P('linkB', 'fileB').absolute()), os.path.join(BASE, 'linkB', 'fileB')) self.assertEqual(str(P('brokenLink').absolute()), os.path.join(BASE, 'brokenLink')) self.assertEqual(str(P('brokenLinkLoop').absolute()), os.path.join(BASE, 'brokenLinkLoop')) # '..' entries should be preserved and not normalised. self.assertEqual(str(P('..').absolute()), os.path.join(BASE, '..')) self.assertEqual(str(P('a', '..').absolute()), os.path.join(BASE, 'a', '..')) self.assertEqual(str(P('..', 'b').absolute()), os.path.join(BASE, '..', 'b')) def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) @unittest.skipIf( pwd is None, reason="Test requires pwd module to get homedir." ) def test_home(self): with os_helper.EnvironmentVarGuard() as env: self._test_home(self.cls.home()) env.clear() env['USERPROFILE'] = os.path.join(BASE, 'userprofile') self._test_home(self.cls.home()) # bpo-38883: ignore `HOME` when set on windows env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) def test_with_segments(self): class P(_BasePurePathSubclass, self.cls): pass with temporary_register("", P): p = P(BASE, session_id=42) self.assertEqual(42, p.absolute().session_id) self.assertEqual(42, p.resolve().session_id) if not is_wasi: # WASI has no user accounts. self.assertEqual(42, p.with_segments('~').expanduser().session_id) self.assertEqual(42, (p / 'fileA').rename(p / 'fileB').session_id) self.assertEqual(42, (p / 'fileB').replace(p / 'fileA').session_id) if os_helper.can_symlink(): self.assertEqual(42, (p / 'linkA').readlink().session_id) for path in p.iterdir(): self.assertEqual(42, path.session_id) for path in p.glob('*'): self.assertEqual(42, path.session_id) for path in p.rglob('*'): self.assertEqual(42, path.session_id) for dirpath, dirnames, filenames in p.walk(): self.assertEqual(42, dirpath.session_id) def test_samefile(self): fileA_path = os.path.join(BASE, 'fileA') fileB_path = os.path.join(BASE, 'dirB', 'fileB') p = self.cls(fileA_path) pp = self.cls(fileA_path) q = self.cls(fileB_path) self.assertTrue(p.samefile(fileA_path)) self.assertTrue(p.samefile(pp)) self.assertFalse(p.samefile(fileB_path)) self.assertFalse(p.samefile(q)) # Test the non-existent file case non_existent = os.path.join(BASE, 'foo') r = self.cls(non_existent) self.assertRaises(FileNotFoundError, p.samefile, r) self.assertRaises(FileNotFoundError, p.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, p) self.assertRaises(FileNotFoundError, r.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, r) self.assertRaises(FileNotFoundError, r.samefile, non_existent) def test_empty_path(self): # The empty path points to '.' p = self.cls('') self.assertEqual(p.stat(), os.stat('.')) @unittest.skipIf(is_wasi, "WASI has no user accounts.") def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') self.assertEqual(p.expanduser(), p) p = P('../~') self.assertEqual(p.expanduser(), p) p = P(P('').absolute().anchor) / '~' self.assertEqual(p.expanduser(), p) p = P('~/a:b') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'), './a:b')) def test_exists(self): P = self.cls p = P(BASE) self.assertIs(True, p.exists()) self.assertIs(True, (p / 'dirA').exists()) self.assertIs(True, (p / 'fileA').exists()) self.assertIs(False, (p / 'fileA' / 'bah').exists()) if os_helper.can_symlink(): self.assertIs(True, (p / 'linkA').exists()) self.assertIs(True, (p / 'linkB').exists()) self.assertIs(True, (p / 'linkB' / 'fileB').exists()) self.assertIs(False, (p / 'linkA' / 'bah').exists()) self.assertIs(False, (p / 'brokenLink').exists()) self.assertIs(True, (p / 'brokenLink').exists(follow_symlinks=False)) self.assertIs(False, (p / 'foo').exists()) self.assertIs(False, P('/xyzzy').exists()) self.assertIs(False, P(BASE + '\udfff').exists()) self.assertIs(False, P(BASE + '\x00').exists()) def test_open_common(self): p = self.cls(BASE) with (p / 'fileA').open('r') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), "this is file A\n") with (p / 'fileA').open('rb') as f: self.assertIsInstance(f, io.BufferedIOBase) self.assertEqual(f.read().strip(), b"this is file A") with (p / 'fileA').open('rb', buffering=0) as f: self.assertIsInstance(f, io.RawIOBase) self.assertEqual(f.read().strip(), b"this is file A") def test_read_write_bytes(self): p = self.cls(BASE) (p / 'fileA').write_bytes(b'abcdefg') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') # Check that trying to write str does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') def test_read_write_text(self): p = self.cls(BASE) (p / 'fileA').write_text('äbcdefg', encoding='latin-1') self.assertEqual((p / 'fileA').read_text( encoding='utf-8', errors='ignore'), 'bcdefg') # Check that trying to write bytes does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes') self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg') def test_write_text_with_newlines(self): p = self.cls(BASE) # Check that `\n` character change nothing (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\nfghlk\n\rmnopq') # Check that `\r` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\rfghlk\r\rmnopq') # Check that `\r\n` character replaces `\n` (p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n') self.assertEqual((p / 'fileA').read_bytes(), b'abcde\r\r\nfghlk\r\n\rmnopq') # Check that no argument passed will change `\n` to `os.linesep` os_linesep_byte = bytes(os.linesep, encoding='ascii') (p / 'fileA').write_text('abcde\nfghlk\n\rmnopq') self.assertEqual((p / 'fileA').read_bytes(), b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq') def test_iterdir(self): P = self.cls p = P(BASE) it = p.iterdir() paths = set(it) expected = ['dirA', 'dirB', 'dirC', 'dirE', 'fileA'] if os_helper.can_symlink(): expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop'] self.assertEqual(paths, { P(BASE, q) for q in expected }) @os_helper.skip_unless_symlink def test_iterdir_symlink(self): # __iter__ on a symlink to a directory. P = self.cls p = P(BASE, 'linkB') paths = set(p.iterdir()) expected = { P(BASE, 'linkB', q) for q in ['fileB', 'linkD'] } self.assertEqual(paths, expected) def test_iterdir_nodir(self): # __iter__ on something that is not a directory. p = self.cls(BASE, 'fileA') with self.assertRaises(OSError) as cm: next(p.iterdir()) # ENOENT or EINVAL under Windows, ENOTDIR otherwise # (see issue #12802). self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) def test_glob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.glob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.glob("fileB"), []) _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) else: _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) if not os_helper.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not os_helper.can_symlink(): _check(p.glob("*/fileB"), ['dirB/fileB']) else: _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) if os_helper.can_symlink(): _check(p.glob("brokenLink"), ['brokenLink']) if not os_helper.can_symlink(): _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) else: _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) def test_glob_case_sensitive(self): P = self.cls def _check(path, pattern, case_sensitive, expected): actual = {str(q) for q in path.glob(pattern, case_sensitive=case_sensitive)} expected = {str(P(BASE, q)) for q in expected} self.assertEqual(actual, expected) path = P(BASE) _check(path, "DIRB/FILE*", True, []) _check(path, "DIRB/FILE*", False, ["dirB/fileB"]) _check(path, "dirb/file*", True, []) _check(path, "dirb/file*", False, ["dirB/fileB"]) def test_rglob_common(self): def _check(glob, expected): self.assertEqual(sorted(glob), sorted(P(BASE, q) for q in expected)) P = self.cls p = P(BASE) it = p.rglob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("**/fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) if not os_helper.can_symlink(): _check(p.rglob("*/fileB"), ["dirB/fileB"]) else: _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) if not os_helper.can_symlink(): _check(p.rglob("*/"), [ "dirA", "dirB", "dirC", "dirC/dirD", "dirE", ]) else: _check(p.rglob("*/"), [ "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", "dirC/dirD", "dirE", "linkB", ]) _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") _check(p.rglob("*"), ["dirC/fileC", "dirC/novel.txt", "dirC/dirD", "dirC/dirD/fileD"]) _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) _check(p.rglob("**/file*"), ["dirC/fileC", "dirC/dirD/fileD"]) _check(p.rglob("dir*/**"), ["dirC/dirD"]) _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) _check(p.rglob("*/"), ["dirC/dirD"]) _check(p.rglob(""), ["dirC", "dirC/dirD"]) _check(p.rglob("**"), ["dirC", "dirC/dirD"]) # gh-91616, a re module regression _check(p.rglob("*.txt"), ["dirC/novel.txt"]) _check(p.rglob("*.*"), ["dirC/novel.txt"]) @os_helper.skip_unless_symlink def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) given = set(p.rglob('*')) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', 'dirC', 'dirC/dirD', 'dirC/dirD/fileD', 'dirC/fileC', 'dirC/novel.txt', 'dirE', 'fileA', 'linkA', 'linkB', 'brokenLinkLoop', } self.assertEqual(given, {p / x for x in expect}) def test_glob_many_open_files(self): depth = 30 P = self.cls base = P(BASE) / 'deep' p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) iters = [base.glob(pattern) for j in range(100)] for it in iters: self.assertEqual(next(it), p) iters = [base.rglob('d') for j in range(100)] p = base for i in range(depth): p = p / 'd' for it in iters: self.assertEqual(next(it), p) def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) self.assertEqual(set(p.glob("../..")), { P(BASE, "..", "..") }) self.assertEqual(set(p.glob("dirA/..")), { P(BASE, "dirA", "..") }) self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) self.assertEqual(set(p.glob("dirA/../file*/..")), set()) self.assertEqual(set(p.glob("../xyzzy")), set()) self.assertEqual(set(p.glob("xyzzy/..")), set()) self.assertEqual(set(p.glob("/".join([".."] * 50))), { P(BASE, *[".."] * 50)}) @os_helper.skip_unless_symlink def test_glob_permissions(self): # See bpo-38894 P = self.cls base = P(BASE) / 'permissions' base.mkdir() self.addCleanup(os_helper.rmtree, base) for i in range(100): link = base / f"link{i}" if i % 2: link.symlink_to(P(BASE, "dirE", "nonexistent")) else: link.symlink_to(P(BASE, "dirC")) self.assertEqual(len(set(base.glob("*"))), 100) self.assertEqual(len(set(base.glob("*/"))), 50) self.assertEqual(len(set(base.glob("*/fileC"))), 50) self.assertEqual(len(set(base.glob("*/file*"))), 50) @os_helper.skip_unless_symlink def test_glob_long_symlink(self): # See gh-87695 base = self.cls(BASE) / 'long_symlink' base.mkdir() bad_link = base / 'bad_link' bad_link.symlink_to("bad" * 200) self.assertEqual(sorted(base.glob('**/*')), [bad_link]) def test_glob_above_recursion_limit(self): recursion_limit = 50 # directory_depth > recursion_limit directory_depth = recursion_limit + 10 base = UPath(os_helper.TESTFN, 'deep') path = UPath(base, *(['d'] * directory_depth)) path.mkdir(parents=True) with set_recursion_limit(recursion_limit): list(base.glob('**')) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) self.assertEqual(q, expected) # This can be used to check both relative and absolute resolutions. _check_resolve_relative = _check_resolve_absolute = _check_resolve @os_helper.skip_unless_symlink def test_resolve_common(self): P = self.cls p = P(BASE, 'foo') with self.assertRaises(OSError) as cm: p.resolve(strict=True) self.assertEqual(cm.exception.errno, errno.ENOENT) # Non-strict self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo')) p = P(BASE, 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo', 'in', 'spam')) p = P(BASE, '..', 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.abspath(os.path.join('foo', 'in', 'spam'))) # These are all relative symlinks. p = P(BASE, 'dirB', 'fileB') self._check_resolve_relative(p, p) p = P(BASE, 'linkA') self._check_resolve_relative(p, P(BASE, 'fileA')) p = P(BASE, 'dirA', 'linkC', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) p = P(BASE, 'dirB', 'linkD', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(BASE, 'dirA', 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) # Now create absolute symlinks. d = os_helper._longpath(tempfile.mkdtemp(suffix='-dirD', dir=os.getcwd())) self.addCleanup(os_helper.rmtree, d) os.symlink(os.path.join(d), join('dirA', 'linkX')) os.symlink(join('dirB'), os.path.join(d, 'linkY')) p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB') self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) @os_helper.skip_unless_symlink def test_resolve_dot(self): # See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/ p = self.cls(BASE) self.dirlink('.', join('0')) self.dirlink(os.path.join('0', '0'), join('1')) self.dirlink(os.path.join('1', '1'), join('2')) q = p / '2' self.assertEqual(q.resolve(strict=True), p) r = q / '3' / '4' self.assertRaises(FileNotFoundError, r.resolve, strict=True) # Non-strict self.assertEqual(r.resolve(strict=False), p / '3' / '4') def test_resolve_nonexist_relative_issue38671(self): p = self.cls('non', 'exist') old_cwd = os.getcwd() os.chdir(BASE) try: self.assertEqual(p.resolve(), self.cls(BASE, p)) finally: os.chdir(old_cwd) def test_with(self): p = self.cls(BASE) it = p.iterdir() it2 = p.iterdir() next(it2) # bpo-46556: path context managers are deprecated in Python 3.11. with self.assertWarns(DeprecationWarning): with p: pass # Using a path as a context manager is a no-op, thus the following # operations should still succeed after the context manage exits. next(it) next(it2) p.exists() p.resolve() p.absolute() with self.assertWarns(DeprecationWarning): with p: pass @os_helper.skip_unless_working_chmod def test_chmod(self): p = self.cls(BASE) / 'fileA' mode = p.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # Set writable bit. new_mode = mode | 0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # On Windows, os.chmod does not follow symlinks (issue #15411) @only_posix @os_helper.skip_unless_working_chmod def test_chmod_follow_symlinks_true(self): p = self.cls(BASE) / 'linkA' q = p.resolve() mode = q.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # Set writable bit new_mode = mode | 0o222 p.chmod(new_mode, follow_symlinks=True) self.assertEqual(q.stat().st_mode, new_mode) # XXX also need a test for lchmod. @os_helper.skip_unless_working_chmod def test_stat(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(p.stat(), st) # Change file mode by flipping write bit. p.chmod(st.st_mode ^ 0o222) self.addCleanup(p.chmod, st.st_mode) self.assertNotEqual(p.stat(), st) @os_helper.skip_unless_symlink def test_stat_no_follow_symlinks(self): p = self.cls(BASE) / 'linkA' st = p.stat() self.assertNotEqual(st, p.stat(follow_symlinks=False)) def test_stat_no_follow_symlinks_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.stat(follow_symlinks=False)) @os_helper.skip_unless_symlink def test_lstat(self): p = self.cls(BASE)/ 'linkA' st = p.stat() self.assertNotEqual(st, p.lstat()) def test_lstat_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.lstat()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") def test_owner(self): p = self.cls(BASE) / 'fileA' uid = p.stat().st_uid try: name = pwd.getpwuid(uid).pw_name except KeyError: self.skipTest( "user %d doesn't have an entry in the system database" % uid) self.assertEqual(name, p.owner()) @unittest.skipUnless(grp, "the grp module is needed for this test") def test_group(self): p = self.cls(BASE) / 'fileA' gid = p.stat().st_gid try: name = grp.getgrgid(gid).gr_name except KeyError: self.skipTest( "group %d doesn't have an entry in the system database" % gid) self.assertEqual(name, p.group()) def test_unlink(self): p = self.cls(BASE) / 'fileA' p.unlink() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) def test_unlink_missing_ok(self): p = self.cls(BASE) / 'fileAAA' self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self): p = self.cls(BASE) / 'dirA' for q in p.iterdir(): q.unlink() p.rmdir() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_hardlink_to(self): P = self.cls(BASE) target = P / 'fileA' size = target.stat().st_size # linking to another path. link = P / 'dirA' / 'fileAA' link.hardlink_to(target) self.assertEqual(link.stat().st_size, size) self.assertTrue(os.path.samefile(target, link)) self.assertTrue(target.exists()) # Linking to a str of a relative path. link2 = P / 'dirA' / 'fileAAA' target2 = rel_join('fileA') link2.hardlink_to(target2) self.assertEqual(os.stat(target2).st_size, size) self.assertTrue(link2.exists()) @unittest.skipIf(hasattr(os, "link"), "os.link() is present") def test_link_to_not_implemented(self): P = self.cls(BASE) p = P / 'fileA' # linking to another path. q = P / 'dirA' / 'fileAA' with self.assertRaises(NotImplementedError): q.hardlink_to(p) def test_rename(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Renaming to another path. q = P / 'dirA' / 'fileAA' renamed_p = p.rename(q) self.assertEqual(renamed_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Renaming to a str of a relative path. r = rel_join('fileAAA') renamed_q = q.rename(r) self.assertEqual(renamed_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) def test_replace(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Replacing a non-existing path. q = P / 'dirA' / 'fileAA' replaced_p = p.replace(q) self.assertEqual(replaced_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Replacing another (existing) path. r = rel_join('dirB', 'fileB') replaced_q = q.replace(r) self.assertEqual(replaced_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) @os_helper.skip_unless_symlink def test_readlink(self): P = self.cls(BASE) self.assertEqual((P / 'linkA').readlink(), self.cls('fileA')) self.assertEqual((P / 'brokenLink').readlink(), self.cls('non-existing')) self.assertEqual((P / 'linkB').readlink(), self.cls('dirB')) with self.assertRaises(OSError): (P / 'fileA').readlink() def test_touch_common(self): P = self.cls(BASE) p = P / 'newfileA' self.assertFalse(p.exists()) p.touch() self.assertTrue(p.exists()) st = p.stat() old_mtime = st.st_mtime old_mtime_ns = st.st_mtime_ns # Rewind the mtime sufficiently far in the past to work around # filesystem-specific timestamp granularity. os.utime(str(p), (old_mtime - 10, old_mtime - 10)) # The file mtime should be refreshed by calling touch() again. p.touch() st = p.stat() self.assertGreaterEqual(st.st_mtime_ns, old_mtime_ns) self.assertGreaterEqual(st.st_mtime, old_mtime) # Now with exist_ok=False. p = P / 'newfileB' self.assertFalse(p.exists()) p.touch(mode=0o700, exist_ok=False) self.assertTrue(p.exists()) self.assertRaises(OSError, p.touch, exist_ok=False) def test_touch_nochange(self): P = self.cls(BASE) p = P / 'fileA' p.touch() with p.open('rb') as f: self.assertEqual(f.read().strip(), b"this is file A") def test_mkdir(self): P = self.cls(BASE) p = P / 'newdirA' self.assertFalse(p.exists()) p.mkdir() self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_parents(self): # Creating a chain of directories. p = self.cls(BASE, 'newdirB', 'newdirC') self.assertFalse(p.exists()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.ENOENT) p.mkdir(parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) # Test `mode` arg. mode = stat.S_IMODE(p.stat().st_mode) # Default mode. p = self.cls(BASE, 'newdirD', 'newdirE') p.mkdir(0o555, parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) if os.name != 'nt': # The directory's permissions follow the mode argument. self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o7555 & mode) # The parent's permissions follow the default process settings. self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode) def test_mkdir_exist_ok(self): p = self.cls(BASE, 'dirB') st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_with_parent(self): p = self.cls(BASE, 'dirC') self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p = p / 'newdirC' p.mkdir(parents=True) st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(parents=True, exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) @unittest.skipIf(is_emscripten, "FS root cannot be modified on Emscripten.") def test_mkdir_exist_ok_root(self): # Issue #25803: A drive root could raise PermissionError on Windows. self.cls('/').resolve().mkdir(exist_ok=True) self.cls('/').resolve().mkdir(parents=True, exist_ok=True) @only_nt # XXX: not sure how to test this on POSIX. def test_mkdir_with_unknown_drive(self): for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA': p = self.cls(d + ':\\') if not p.is_dir(): break else: self.skipTest("cannot find a drive that doesn't exist") with self.assertRaises(OSError): (p / 'child' / 'path').mkdir(parents=True) def test_mkdir_with_child_file(self): p = self.cls(BASE, 'dirB', 'fileB') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True, exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_no_parents_file(self): p = self.cls(BASE, 'fileA') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_concurrent_parent_creation(self): for pattern_num in range(32): p = self.cls(BASE, 'dirCPC%d' % pattern_num) self.assertFalse(p.exists()) real_mkdir = os.mkdir def my_mkdir(path, mode=0o777): path = str(path) # Emulate another process that would create the directory # just before we try to create it ourselves. We do it # in all possible pattern combinations, assuming that this # function is called at most 5 times (dirCPC/dir1/dir2, # dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2). if pattern.pop(): real_mkdir(path, mode) # From another process. concurrently_created.add(path) real_mkdir(path, mode) # Our real call. pattern = [bool(pattern_num & (1 << n)) for n in range(5)] concurrently_created = set() p12 = p / 'dir1' / 'dir2' try: with mock.patch("os.mkdir", my_mkdir): p12.mkdir(parents=True, exist_ok=False) except FileExistsError: self.assertIn(str(p12), concurrently_created) else: self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) @os_helper.skip_unless_symlink def test_symlink_to(self): P = self.cls(BASE) target = P / 'fileA' # Symlinking a path target. link = P / 'dirA' / 'linkAA' link.symlink_to(target) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) # Symlinking a str target. link = P / 'dirA' / 'linkAAA' link.symlink_to(str(target)) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertFalse(link.is_dir()) # Symlinking to a directory. target = P / 'dirB' link = P / 'dirA' / 'linkAAAA' link.symlink_to(target, target_is_directory=True) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertTrue(link.is_dir()) self.assertTrue(list(link.iterdir())) def test_is_dir(self): P = self.cls(BASE) self.assertTrue((P / 'dirA').is_dir()) self.assertFalse((P / 'fileA').is_dir()) self.assertFalse((P / 'non-existing').is_dir()) self.assertFalse((P / 'fileA' / 'bah').is_dir()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_dir()) self.assertTrue((P / 'linkB').is_dir()) self.assertFalse((P/ 'brokenLink').is_dir(), False) self.assertIs((P / 'dirA\udfff').is_dir(), False) self.assertIs((P / 'dirA\x00').is_dir(), False) def test_is_file(self): P = self.cls(BASE) self.assertTrue((P / 'fileA').is_file()) self.assertFalse((P / 'dirA').is_file()) self.assertFalse((P / 'non-existing').is_file()) self.assertFalse((P / 'fileA' / 'bah').is_file()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_file()) self.assertFalse((P / 'linkB').is_file()) self.assertFalse((P/ 'brokenLink').is_file()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) def test_is_mount(self): P = self.cls(BASE) if os.name == 'nt': R = self.cls('c:\\') else: R = self.cls('/') self.assertFalse((P / 'fileA').is_mount()) self.assertFalse((P / 'dirA').is_mount()) self.assertFalse((P / 'non-existing').is_mount()) self.assertFalse((P / 'fileA' / 'bah').is_mount()) self.assertTrue(R.is_mount()) if os_helper.can_symlink(): self.assertFalse((P / 'linkA').is_mount()) self.assertIs((R / '\udfff').is_mount(), False) def test_is_symlink(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_symlink()) self.assertFalse((P / 'dirA').is_symlink()) self.assertFalse((P / 'non-existing').is_symlink()) self.assertFalse((P / 'fileA' / 'bah').is_symlink()) if os_helper.can_symlink(): self.assertTrue((P / 'linkA').is_symlink()) self.assertTrue((P / 'linkB').is_symlink()) self.assertTrue((P/ 'brokenLink').is_symlink()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) if os_helper.can_symlink(): self.assertIs((P / 'linkA\udfff').is_file(), False) self.assertIs((P / 'linkA\x00').is_file(), False) def test_is_junction(self): P = self.cls(BASE) with mock.patch.object(P._flavour, 'isjunction'): self.assertEqual(P.is_junction(), P._flavour.isjunction.return_value) P._flavour.isjunction.assert_called_once_with(P) def test_is_fifo_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_fifo()) self.assertFalse((P / 'dirA').is_fifo()) self.assertFalse((P / 'non-existing').is_fifo()) self.assertFalse((P / 'fileA' / 'bah').is_fifo()) self.assertIs((P / 'fileA\udfff').is_fifo(), False) self.assertIs((P / 'fileA\x00').is_fifo(), False) @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required") @unittest.skipIf(sys.platform == "vxworks", "fifo requires special path on VxWorks") def test_is_fifo_true(self): P = self.cls(BASE, 'myfifo') try: os.mkfifo(str(P)) except PermissionError as e: self.skipTest('os.mkfifo(): %s' % e) self.assertTrue(P.is_fifo()) self.assertFalse(P.is_socket()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'myfifo\udfff').is_fifo(), False) self.assertIs(self.cls(BASE, 'myfifo\x00').is_fifo(), False) def test_is_socket_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_socket()) self.assertFalse((P / 'dirA').is_socket()) self.assertFalse((P / 'non-existing').is_socket()) self.assertFalse((P / 'fileA' / 'bah').is_socket()) self.assertIs((P / 'fileA\udfff').is_socket(), False) self.assertIs((P / 'fileA\x00').is_socket(), False) @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") @unittest.skipIf( is_emscripten, "Unix sockets are not implemented on Emscripten." ) @unittest.skipIf( is_wasi, "Cannot create socket on WASI." ) def test_is_socket_true(self): P = self.cls(BASE, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.addCleanup(sock.close) try: sock.bind(str(P)) except OSError as e: if (isinstance(e, PermissionError) or "AF_UNIX path too long" in str(e)): self.skipTest("cannot bind Unix socket: " + str(e)) self.assertTrue(P.is_socket()) self.assertFalse(P.is_fifo()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'mysock\udfff').is_socket(), False) self.assertIs(self.cls(BASE, 'mysock\x00').is_socket(), False) def test_is_block_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_block_device()) self.assertFalse((P / 'dirA').is_block_device()) self.assertFalse((P / 'non-existing').is_block_device()) self.assertFalse((P / 'fileA' / 'bah').is_block_device()) self.assertIs((P / 'fileA\udfff').is_block_device(), False) self.assertIs((P / 'fileA\x00').is_block_device(), False) def test_is_char_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_char_device()) self.assertFalse((P / 'dirA').is_char_device()) self.assertFalse((P / 'non-existing').is_char_device()) self.assertFalse((P / 'fileA' / 'bah').is_char_device()) self.assertIs((P / 'fileA\udfff').is_char_device(), False) self.assertIs((P / 'fileA\x00').is_char_device(), False) def test_is_char_device_true(self): # Under Unix, /dev/null should generally be a char device. P = self.cls('/dev/null') if not P.exists(): self.skipTest("/dev/null required") self.assertTrue(P.is_char_device()) self.assertFalse(P.is_block_device()) self.assertFalse(P.is_file()) self.assertIs(self.cls('/dev/null\udfff').is_char_device(), False) self.assertIs(self.cls('/dev/null\x00').is_char_device(), False) def test_pickling_common(self): p = self.cls(BASE, 'fileA') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertEqual(pp.stat(), p.stat()) def test_parts_interning(self): P = self.cls p = P('/usr/bin/foo') q = P('/usr/local/bin') # 'usr' self.assertIs(p.parts[1], q.parts[1]) # 'bin' self.assertIs(p.parts[2], q.parts[3]) def _check_complex_symlinks(self, link0_target): # Test solving a non-looping chain of symlinks (issue #19887). P = self.cls(BASE) self.dirlink(os.path.join('link0', 'link0'), join('link1')) self.dirlink(os.path.join('link1', 'link1'), join('link2')) self.dirlink(os.path.join('link2', 'link2'), join('link3')) self.dirlink(link0_target, join('link0')) # Resolve absolute paths. p = (P / 'link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) # Resolve relative paths. old_path = os.getcwd() os.chdir(BASE) try: p = self.cls('link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) finally: os.chdir(old_path) @os_helper.skip_unless_symlink def test_complex_symlinks_absolute(self): self._check_complex_symlinks(BASE) @os_helper.skip_unless_symlink def test_complex_symlinks_relative(self): self._check_complex_symlinks('.') @os_helper.skip_unless_symlink def test_complex_symlinks_relative_dot_dot(self): self._check_complex_symlinks(os.path.join('dirA', '..')) # def test_passing_kwargs_deprecated(self): # with self.assertWarns(DeprecationWarning): # self.cls(foo="bar") class WalkTests(unittest.TestCase): def setUp(self): self.addCleanup(os_helper.rmtree, os_helper.TESTFN) # Build: # TESTFN/ # TEST1/ a file kid and two directory kids # tmp1 # SUB1/ a file kid and a directory kid # tmp2 # SUB11/ no kids # SUB2/ a file kid and a dirsymlink kid # tmp3 # SUB21/ not readable # tmp5 # link/ a symlink to TEST2 # broken_link # broken_link2 # broken_link3 # TEST2/ # tmp4 a lone file self.walk_path = UPath(os_helper.TESTFN, "TEST1") self.sub1_path = self.walk_path / "SUB1" self.sub11_path = self.sub1_path / "SUB11" self.sub2_path = self.walk_path / "SUB2" sub21_path= self.sub2_path / "SUB21" tmp1_path = self.walk_path / "tmp1" tmp2_path = self.sub1_path / "tmp2" tmp3_path = self.sub2_path / "tmp3" tmp5_path = sub21_path / "tmp3" self.link_path = self.sub2_path / "link" t2_path = UPath(os_helper.TESTFN, "TEST2") tmp4_path = UPath(os_helper.TESTFN, "TEST2", "tmp4") broken_link_path = self.sub2_path / "broken_link" broken_link2_path = self.sub2_path / "broken_link2" broken_link3_path = self.sub2_path / "broken_link3" os.makedirs(self.sub11_path) os.makedirs(self.sub2_path) os.makedirs(sub21_path) os.makedirs(t2_path) for path in tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path: with open(path, "x", encoding='utf-8') as f: f.write(f"I'm {path} and proud of it. Blame test_pathlib.\n") if os_helper.can_symlink(): os.symlink(os.path.abspath(t2_path), self.link_path) os.symlink('broken', broken_link_path, True) os.symlink(UPath('tmp3', 'broken'), broken_link2_path, True) os.symlink(UPath('SUB21', 'tmp5'), broken_link3_path, True) self.sub2_tree = (self.sub2_path, ["SUB21"], ["broken_link", "broken_link2", "broken_link3", "link", "tmp3"]) else: self.sub2_tree = (self.sub2_path, ["SUB21"], ["tmp3"]) if not is_emscripten: # Emscripten fails with inaccessible directories. os.chmod(sub21_path, 0) try: os.listdir(sub21_path) except PermissionError: self.addCleanup(os.chmod, sub21_path, stat.S_IRWXU) else: os.chmod(sub21_path, stat.S_IRWXU) os.unlink(tmp5_path) os.rmdir(sub21_path) del self.sub2_tree[1][:1] def test_walk_topdown(self): walker = self.walk_path.walk() entry = next(walker) entry[1].sort() # Ensure we visit SUB1 before SUB2 self.assertEqual(entry, (self.walk_path, ["SUB1", "SUB2"], ["tmp1"])) entry = next(walker) self.assertEqual(entry, (self.sub1_path, ["SUB11"], ["tmp2"])) entry = next(walker) self.assertEqual(entry, (self.sub11_path, [], [])) entry = next(walker) entry[1].sort() entry[2].sort() self.assertEqual(entry, self.sub2_tree) with self.assertRaises(StopIteration): next(walker) def test_walk_prune(self, walk_path=None): if walk_path is None: walk_path = self.walk_path # Prune the search. all = [] for root, dirs, files in walk_path.walk(): all.append((root, dirs, files)) if 'SUB1' in dirs: # Note that this also mutates the dirs we appended to all! dirs.remove('SUB1') self.assertEqual(len(all), 2) self.assertEqual(all[0], (self.walk_path, ["SUB2"], ["tmp1"])) all[1][-1].sort() all[1][1].sort() self.assertEqual(all[1], self.sub2_tree) def test_file_like_path(self): self.test_walk_prune(FakePath(self.walk_path).__fspath__()) def test_walk_bottom_up(self): seen_testfn = seen_sub1 = seen_sub11 = seen_sub2 = False for path, dirnames, filenames in self.walk_path.walk(top_down=False): if path == self.walk_path: self.assertFalse(seen_testfn) self.assertTrue(seen_sub1) self.assertTrue(seen_sub2) self.assertEqual(sorted(dirnames), ["SUB1", "SUB2"]) self.assertEqual(filenames, ["tmp1"]) seen_testfn = True elif path == self.sub1_path: self.assertFalse(seen_testfn) self.assertFalse(seen_sub1) self.assertTrue(seen_sub11) self.assertEqual(dirnames, ["SUB11"]) self.assertEqual(filenames, ["tmp2"]) seen_sub1 = True elif path == self.sub11_path: self.assertFalse(seen_sub1) self.assertFalse(seen_sub11) self.assertEqual(dirnames, []) self.assertEqual(filenames, []) seen_sub11 = True elif path == self.sub2_path: self.assertFalse(seen_testfn) self.assertFalse(seen_sub2) self.assertEqual(sorted(dirnames), sorted(self.sub2_tree[1])) self.assertEqual(sorted(filenames), sorted(self.sub2_tree[2])) seen_sub2 = True else: raise AssertionError(f"Unexpected path: {path}") self.assertTrue(seen_testfn) @os_helper.skip_unless_symlink def test_walk_follow_symlinks(self): walk_it = self.walk_path.walk(follow_symlinks=True) for root, dirs, files in walk_it: if root == self.link_path: self.assertEqual(dirs, []) self.assertEqual(files, ["tmp4"]) break else: self.fail("Didn't follow symlink with follow_symlinks=True") @os_helper.skip_unless_symlink def test_walk_symlink_location(self): # Tests whether symlinks end up in filenames or dirnames depending # on the `follow_symlinks` argument. walk_it = self.walk_path.walk(follow_symlinks=False) for root, dirs, files in walk_it: if root == self.sub2_path: self.assertIn("link", files) break else: self.fail("symlink not found") walk_it = self.walk_path.walk(follow_symlinks=True) for root, dirs, files in walk_it: if root == self.sub2_path: self.assertIn("link", dirs) break def test_walk_bad_dir(self): errors = [] walk_it = self.walk_path.walk(on_error=errors.append) root, dirs, files = next(walk_it) self.assertEqual(errors, []) dir1 = 'SUB1' path1 = root / dir1 path1new = (root / dir1).with_suffix(".new") path1.rename(path1new) try: roots = [r for r, _, _ in walk_it] self.assertTrue(errors) self.assertNotIn(path1, roots) self.assertNotIn(path1new, roots) for dir2 in dirs: if dir2 != dir1: self.assertIn(root / dir2, roots) finally: path1new.rename(path1) def test_walk_many_open_files(self): depth = 30 base = UPath(os_helper.TESTFN, 'deep') path = UPath(base, *(['d']*depth)) path.mkdir(parents=True) iters = [base.walk(top_down=False) for _ in range(100)] for i in range(depth + 1): expected = (path, ['d'] if i else [], []) for it in iters: self.assertEqual(next(it), expected) path = path.parent iters = [base.walk(top_down=True) for _ in range(100)] path = base for i in range(depth + 1): expected = (path, ['d'] if i < depth else [], []) for it in iters: self.assertEqual(next(it), expected) path = path / 'd' def test_walk_above_recursion_limit(self): recursion_limit = 50 # directory_depth > recursion_limit directory_depth = recursion_limit + 10 base = UPath(os_helper.TESTFN, 'deep') path = UPath(base, *(['d'] * directory_depth)) path.mkdir(parents=True) with set_recursion_limit(recursion_limit): list(base.walk()) list(base.walk(top_down=False)) class PathTest(_BasePathTest, unittest.TestCase): cls = UPath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), WindowsUPath if os.name == 'nt' else PosixUPath) def test_unsupported_flavour(self): if os.name == 'nt': self.assertRaises(NotImplementedError, PosixUPath) else: self.assertRaises(NotImplementedError, WindowsUPath) def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): list(p.glob('')) def test_with_segments(self): if self.cls is UPath: pytest.skip(reason="") super().test_with_segments() @only_posix class PosixPathTest(_BasePathTest, unittest.TestCase): cls = PosixUPath def test_absolute(self): P = self.cls self.assertEqual(str(P('/').absolute()), '/') self.assertEqual(str(P('/a').absolute()), '/a') self.assertEqual(str(P('/a/b').absolute()), '/a/b') # '//'-prefixed absolute path (supported by POSIX). self.assertEqual(str(P('//').absolute()), '//') self.assertEqual(str(P('//a').absolute()), '//a') self.assertEqual(str(P('//a/b').absolute()), '//a/b') def _check_symlink_loop(self, *args, strict=True): path = self.cls(*args) with self.assertRaises(RuntimeError): print(path.resolve(strict)) @unittest.skipIf( is_emscripten or is_wasi, "umask is not implemented on Emscripten/WASI." ) def test_open_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) with (p / 'new_file').open('wb'): pass st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) with (p / 'other_new_file').open('wb'): pass st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) def test_resolve_root(self): current_directory = os.getcwd() try: os.chdir('/') p = self.cls('spam') self.assertEqual(str(p.resolve()), '/spam') finally: os.chdir(current_directory) @unittest.skipIf( is_emscripten or is_wasi, "umask is not implemented on Emscripten/WASI." ) def test_touch_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) (p / 'new_file').touch() st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) (p / 'other_new_file').touch() st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) (p / 'masked_new_file').touch(mode=0o750) st = os.stat(join('masked_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o750) @os_helper.skip_unless_symlink def test_resolve_loop(self): # Loops with relative symlinks. os.symlink('linkX/inside', join('linkX')) self._check_symlink_loop(BASE, 'linkX') os.symlink('linkY', join('linkY')) self._check_symlink_loop(BASE, 'linkY') os.symlink('linkZ/../linkZ', join('linkZ')) self._check_symlink_loop(BASE, 'linkZ') # Non-strict self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False) # Loops with absolute symlinks. os.symlink(join('linkU/inside'), join('linkU')) self._check_symlink_loop(BASE, 'linkU') os.symlink(join('linkV'), join('linkV')) self._check_symlink_loop(BASE, 'linkV') os.symlink(join('linkW/../linkW'), join('linkW')) self._check_symlink_loop(BASE, 'linkW') # Non-strict self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False) def test_glob(self): P = self.cls p = P(BASE) given = set(p.glob("FILEa")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.glob("FILEa*")), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") given = set(p.rglob("FILEd")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.rglob("FILEd*")), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') @unittest.skipIf(sys.platform == "vxworks", "no home directory on VxWorks") def test_expanduser(self): P = self.cls import_helper.import_module('pwd') import pwd pwdent = pwd.getpwuid(os.getuid()) username = pwdent.pw_name userhome = pwdent.pw_dir.rstrip('/') or '/' # Find arbitrary different user (if exists). for pwdent in pwd.getpwall(): othername = pwdent.pw_name otherhome = pwdent.pw_dir.rstrip('/') if othername != username and otherhome: break else: othername = username otherhome = userhome fakename = 'fakeuser' # This user can theoretically exist on a test runner. Create unique name: try: while pwd.getpwnam(fakename): fakename += '1' except KeyError: pass # Non-existent name found p1 = P('~/Documents') p2 = P(f'~{username}/Documents') p3 = P(f'~{othername}/Documents') p4 = P(f'../~{username}/Documents') p5 = P(f'/~{username}/Documents') p6 = P('') p7 = P(f'~{fakename}/Documents') with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) self.assertEqual(p1.expanduser(), P(userhome) / 'Documents') self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) env['HOME'] = '/tmp' self.assertEqual(p1.expanduser(), P('/tmp/Documents')) self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) @unittest.skipIf(sys.platform != "darwin", "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: file_descriptors = list(UPath('/dev/fd').rglob("*"))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee # which one will fail. for f in file_descriptors: f.exists() f.is_dir() f.is_file() f.is_symlink() f.is_block_device() f.is_char_device() f.is_fifo() f.is_socket() except OSError as e: if e.errno == errno.EBADF: self.fail("Bad file descriptor not handled.") raise @only_nt class WindowsPathTest(_BasePathTest, unittest.TestCase): cls = WindowsUPath def test_absolute(self): P = self.cls # Simple absolute paths. self.assertEqual(str(P('c:\\').absolute()), 'c:\\') self.assertEqual(str(P('c:\\a').absolute()), 'c:\\a') self.assertEqual(str(P('c:\\a\\b').absolute()), 'c:\\a\\b') # UNC absolute paths. share = '\\\\server\\share\\' self.assertEqual(str(P(share).absolute()), share) self.assertEqual(str(P(share + 'a').absolute()), share + 'a') self.assertEqual(str(P(share + 'a\\b').absolute()), share + 'a\\b') # UNC relative paths. with mock.patch("os.getcwd") as getcwd: getcwd.return_value = share self.assertEqual(str(P().absolute()), share) self.assertEqual(str(P('.').absolute()), share) self.assertEqual(str(P('a').absolute()), os.path.join(share, 'a')) self.assertEqual(str(P('a', 'b', 'c').absolute()), os.path.join(share, 'a', 'b', 'c')) drive = os.path.splitdrive(BASE)[0] with os_helper.change_cwd(BASE): # Relative path with root self.assertEqual(str(P('\\').absolute()), drive + '\\') self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo') # Relative path on current drive self.assertEqual(str(P(drive).absolute()), BASE) self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(BASE, 'foo')) with os_helper.subst_drive(BASE) as other_drive: # Set the working directory on the substitute drive saved_cwd = os.getcwd() other_cwd = f'{other_drive}\\dirA' os.chdir(other_cwd) os.chdir(saved_cwd) # Relative path on another drive self.assertEqual(str(P(other_drive).absolute()), other_cwd) self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo') def test_glob(self): P = self.cls p = P(BASE) self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) self.assertEqual(set(p.glob("*a\\")), { P(BASE, "dirA") }) self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\fileA"}) self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(BASE, "dirC/dirD") }) self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\fileD"}) def test_expanduser(self): P = self.cls with os_helper.EnvironmentVarGuard() as env: env.pop('HOME', None) env.pop('USERPROFILE', None) env.pop('HOMEPATH', None) env.pop('HOMEDRIVE', None) env['USERNAME'] = 'alice' # test that the path returns unchanged p1 = P('~/My Documents') p2 = P('~alice/My Documents') p3 = P('~bob/My Documents') p4 = P('/~/My Documents') p5 = P('d:~/My Documents') p6 = P('') self.assertRaises(RuntimeError, p1.expanduser) self.assertRaises(RuntimeError, p2.expanduser) self.assertRaises(RuntimeError, p3.expanduser) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) def check(): env.pop('USERNAME', None) self.assertEqual(p1.expanduser(), P('C:/Users/alice/My Documents')) self.assertRaises(RuntimeError, p2.expanduser) env['USERNAME'] = 'alice' self.assertEqual(p2.expanduser(), P('C:/Users/alice/My Documents')) self.assertEqual(p3.expanduser(), P('C:/Users/bob/My Documents')) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) env['HOMEPATH'] = 'C:\\Users\\alice' check() env['HOMEDRIVE'] = 'C:\\' env['HOMEPATH'] = 'Users\\alice' check() env.pop('HOMEDRIVE', None) env.pop('HOMEPATH', None) env['USERPROFILE'] = 'C:\\Users\\alice' check() # bpo-38883: ignore `HOME` when set on windows env['HOME'] = 'C:\\Users\\eve' check() class PathSubclassTest(_BasePathTest, unittest.TestCase): class cls(WindowsUPath if os.name == 'nt' else PosixUPath): pass def setUp(self): self._registry_cm = temporary_register("", self.cls) self._registry_cm.__enter__() return super().setUp() def tearDown(self): self._registry_cm.__exit__(None, None, None) return super().tearDown() # repr() roundtripping is not supported in custom subclass. test_repr_roundtrips = None def test_with_segments(self): super().test_with_segments() universal_pathlib-0.3.10/upath/tests/pathlib/test_pathlib_39.py000066400000000000000000003006471514661127100246010ustar00rootroot00000000000000import collections.abc import io import os import sys import errno import pathlib import pickle import socket import stat import tempfile import unittest from unittest import mock from . import _test_support as support from ._test_support import TESTFN, FakePath try: import grp, pwd except ImportError: grp = pwd = None from upath.core import UPath from upath.implementations.local import PosixUPath, WindowsUPath import pytest pytestmark = pytest.mark.skipif(sys.version_info[:2] != (3, 9), reason="py39 only") # # Tests for the pure classes. # class _BasePurePathTest(object): # Keys are canonical paths, values are list of tuples of arguments # supposed to produce equal paths. equivalences = { 'a/b': [ ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'), ('a/b/',), ('a//b',), ('a//b//',), # Empty components get removed. ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''), ], '/b/c/d': [ ('a', '/b/c', 'd'), ('a', '///b//c', 'd/'), ('/a', '/b/c', 'd'), # Empty components get removed. ('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'), ], } def setUp(self): p = self.cls('a') self.flavour = p._flavour self.sep = self.flavour.sep self.altsep = self.flavour.altsep def test_constructor_common(self): P = self.cls p = P('a') self.assertIsInstance(p, P) P('a', 'b', 'c') P('/a', 'b', 'c') P('a/b/c') P('/a/b/c') P(FakePath("a/b/c")) self.assertEqual(P(P('a')), P('a')) self.assertEqual(P(P('a'), 'b'), P('a/b')) self.assertEqual(P(P('a'), P('b')), P('a/b')) self.assertEqual(P(P('a'), P('b'), P('c')), P(FakePath("a/b/c"))) def _check_str_subclass(self, *args): # Issue #21127: it should be possible to construct a PurePath object # from a str subclass instance, and it then gets converted to # a pure str object. class StrSubclass(str): pass P = self.cls p = P(*(StrSubclass(x) for x in args)) self.assertEqual(p, P(*args)) for part in p.parts: self.assertIs(type(part), str) def test_str_subclass_common(self): self._check_str_subclass('') self._check_str_subclass('.') self._check_str_subclass('a') self._check_str_subclass('a/b.txt') self._check_str_subclass('/a/b.txt') def test_join_common(self): P = self.cls p = P('a/b') pp = p.joinpath('c') self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p.joinpath('c', 'd') self.assertEqual(pp, P('a/b/c/d')) pp = p.joinpath(P('c')) self.assertEqual(pp, P('a/b/c')) pp = p.joinpath('/c') self.assertEqual(pp, P('/c')) def test_div_common(self): # Basically the same as joinpath(). P = self.cls p = P('a/b') pp = p / 'c' self.assertEqual(pp, P('a/b/c')) self.assertIs(type(pp), type(p)) pp = p / 'c/d' self.assertEqual(pp, P('a/b/c/d')) pp = p / 'c' / 'd' self.assertEqual(pp, P('a/b/c/d')) pp = 'c' / p / 'd' self.assertEqual(pp, P('c/a/b/d')) pp = p / P('c') self.assertEqual(pp, P('a/b/c')) pp = p/ '/c' self.assertEqual(pp, P('/c')) def _check_str(self, expected, args): p = self.cls(*args) self.assertEqual(str(p), expected.replace('/', self.sep)) def test_str_common(self): # Canonicalized paths roundtrip. for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self._check_str(pathstr, (pathstr,)) # Special case for the empty path. self._check_str('.', ('',)) # Other tests for str() are in test_equivalences(). def test_as_posix_common(self): P = self.cls for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): self.assertEqual(P(pathstr).as_posix(), pathstr) # Other tests for as_posix() are in test_equivalences(). def test_as_bytes_common(self): sep = os.fsencode(self.sep) P = self.cls self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b') def test_as_uri_common(self): P = self.cls with self.assertRaises(ValueError): P('a').as_uri() with self.assertRaises(ValueError): P().as_uri() def test_repr_common(self): for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): p = self.cls(pathstr) clsname = p.__class__.__name__ r = repr(p) # The repr() is in the form ClassName("forward-slashes path"). self.assertTrue(r.startswith(clsname + '('), r) self.assertTrue(r.endswith(')'), r) inner = r[len(clsname) + 1 : -1] self.assertEqual(eval(inner), p.as_posix()) # The repr() roundtrips. q = eval(r, {"PosixUPath": PosixUPath, "WindowsUPath": WindowsUPath}) self.assertIs(q.__class__, p.__class__) self.assertEqual(q, p) self.assertEqual(repr(q), r) def test_eq_common(self): P = self.cls self.assertEqual(P('a/b'), P('a/b')) self.assertEqual(P('a/b'), P('a', 'b')) self.assertNotEqual(P('a/b'), P('a')) self.assertNotEqual(P('a/b'), P('/a/b')) self.assertNotEqual(P('a/b'), P()) self.assertNotEqual(P('/a/b'), P('/')) self.assertNotEqual(P(), P('/')) self.assertNotEqual(P(), "") self.assertNotEqual(P(), {}) self.assertNotEqual(P(), int) def test_match_common(self): P = self.cls self.assertRaises(ValueError, P('a').match, '') self.assertRaises(ValueError, P('a').match, '.') # Simple relative pattern. self.assertTrue(P('b.py').match('b.py')) self.assertTrue(P('a/b.py').match('b.py')) self.assertTrue(P('/a/b.py').match('b.py')) self.assertFalse(P('a.py').match('b.py')) self.assertFalse(P('b/py').match('b.py')) self.assertFalse(P('/a.py').match('b.py')) self.assertFalse(P('b.py/c').match('b.py')) # Wildcard relative pattern. self.assertTrue(P('b.py').match('*.py')) self.assertTrue(P('a/b.py').match('*.py')) self.assertTrue(P('/a/b.py').match('*.py')) self.assertFalse(P('b.pyc').match('*.py')) self.assertFalse(P('b./py').match('*.py')) self.assertFalse(P('b.py/c').match('*.py')) # Multi-part relative pattern. self.assertTrue(P('ab/c.py').match('a*/*.py')) self.assertTrue(P('/d/ab/c.py').match('a*/*.py')) self.assertFalse(P('a.py').match('a*/*.py')) self.assertFalse(P('/dab/c.py').match('a*/*.py')) self.assertFalse(P('ab/c.py/d').match('a*/*.py')) # Absolute pattern. self.assertTrue(P('/b.py').match('/*.py')) self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('a/b.py').match('/*.py')) self.assertFalse(P('/a/b.py').match('/*.py')) # Multi-part absolute pattern. self.assertTrue(P('/a/b.py').match('/a/*.py')) self.assertFalse(P('/ab.py').match('/a/*.py')) self.assertFalse(P('/a/b/c.py').match('/a/*.py')) # Multi-part glob-style pattern. self.assertFalse(P('/a/b/c.py').match('/**/*.py')) self.assertTrue(P('/a/b/c.py').match('/a/**/*.py')) def test_ordering_common(self): # Ordering is tuple-alike. def assertLess(a, b): self.assertLess(a, b) self.assertGreater(b, a) P = self.cls a = P('a') b = P('a/b') c = P('abc') d = P('b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) P = self.cls a = P('/a') b = P('/a/b') c = P('/abc') d = P('/b') assertLess(a, b) assertLess(a, c) assertLess(a, d) assertLess(b, c) assertLess(c, d) with self.assertRaises(TypeError): P() < {} def test_parts_common(self): # `parts` returns a tuple. sep = self.sep P = self.cls p = P('a/b') parts = p.parts self.assertEqual(parts, ('a', 'b')) # The object gets reused. self.assertIs(parts, p.parts) # When the path is absolute, the anchor is a separate part. p = P('/a/b') parts = p.parts self.assertEqual(parts, (sep, 'a', 'b')) def test_fspath_common(self): P = self.cls p = P('a/b') self._check_str(p.__fspath__(), ('a/b',)) self._check_str(os.fspath(p), ('a/b',)) def test_equivalences(self): for k, tuples in self.equivalences.items(): canon = k.replace('/', self.sep) posix = k.replace(self.sep, '/') if canon != posix: tuples = tuples + [ tuple(part.replace('/', self.sep) for part in t) for t in tuples ] tuples.append((posix, )) pcanon = self.cls(canon) for t in tuples: p = self.cls(*t) self.assertEqual(p, pcanon, "failed with args {}".format(t)) self.assertEqual(hash(p), hash(pcanon)) self.assertEqual(str(p), canon) self.assertEqual(p.as_posix(), posix) def test_parent_common(self): # Relative P = self.cls p = P('a/b/c') self.assertEqual(p.parent, P('a/b')) self.assertEqual(p.parent.parent, P('a')) self.assertEqual(p.parent.parent.parent, P()) self.assertEqual(p.parent.parent.parent.parent, P()) # Anchored p = P('/a/b/c') self.assertEqual(p.parent, P('/a/b')) self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative P = self.cls p = P('a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('a/b')) self.assertEqual(par[1], P('a')) self.assertEqual(par[2], P('.')) self.assertEqual(list(par), [P('a/b'), P('a'), P('.')]) with self.assertRaises(IndexError): par[-1] with self.assertRaises(IndexError): par[3] with self.assertRaises(TypeError): par[0] = p # Anchored p = P('/a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('/a/b')) self.assertEqual(par[1], P('/a')) self.assertEqual(par[2], P('/')) self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) with self.assertRaises(IndexError): par[3] def test_drive_common(self): P = self.cls self.assertEqual(P('a/b').drive, '') self.assertEqual(P('/a/b').drive, '') self.assertEqual(P('').drive, '') def test_root_common(self): P = self.cls sep = self.sep self.assertEqual(P('').root, '') self.assertEqual(P('a/b').root, '') self.assertEqual(P('/').root, sep) self.assertEqual(P('/a/b').root, sep) def test_anchor_common(self): P = self.cls sep = self.sep self.assertEqual(P('').anchor, '') self.assertEqual(P('a/b').anchor, '') self.assertEqual(P('/').anchor, sep) self.assertEqual(P('/a/b').anchor, sep) def test_name_common(self): P = self.cls self.assertEqual(P('').name, '') self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') self.assertEqual(P('/a/b').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') def test_suffix_common(self): P = self.cls self.assertEqual(P('').suffix, '') self.assertEqual(P('.').suffix, '') self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') self.assertEqual(P('/a/b').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') self.assertEqual(P('a/.hgrc').suffix, '') self.assertEqual(P('/a/.hgrc').suffix, '') self.assertEqual(P('a/.hg.rc').suffix, '.rc') self.assertEqual(P('/a/.hg.rc').suffix, '.rc') self.assertEqual(P('a/b.tar.gz').suffix, '.gz') self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') def test_suffixes_common(self): P = self.cls self.assertEqual(P('').suffixes, []) self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) self.assertEqual(P('a/.hgrc').suffixes, []) self.assertEqual(P('/a/.hgrc').suffixes, []) self.assertEqual(P('a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) def test_stem_common(self): P = self.cls self.assertEqual(P('').stem, '') self.assertEqual(P('.').stem, '') self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name_common(self): P = self.cls self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') self.assertRaises(ValueError, P('a/b').with_name, '') self.assertRaises(ValueError, P('a/b').with_name, '/c') self.assertRaises(ValueError, P('a/b').with_name, 'c/') self.assertRaises(ValueError, P('a/b').with_name, 'c/d') def test_with_stem_common(self): P = self.cls self.assertEqual(P('a/b').with_stem('d'), P('a/d')) self.assertEqual(P('/a/b').with_stem('d'), P('/a/d')) self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py')) self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py')) self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') self.assertRaises(ValueError, P('a/b').with_stem, '') self.assertRaises(ValueError, P('a/b').with_stem, '/c') self.assertRaises(ValueError, P('a/b').with_stem, 'c/') self.assertRaises(ValueError, P('a/b').with_stem, 'c/d') def test_with_suffix_common(self): P = self.cls self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('a/b').with_suffix, '/') self.assertRaises(ValueError, P('a/b').with_suffix, '.') self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d') self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(ValueError, P('a/b').with_suffix, (self.flavour.sep, 'd')) def test_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.relative_to) self.assertRaises(TypeError, p.relative_to, b'a') self.assertEqual(p.relative_to(P()), P('a/b')) self.assertEqual(p.relative_to(''), P('a/b')) self.assertEqual(p.relative_to(P('a')), P('b')) self.assertEqual(p.relative_to('a'), P('b')) self.assertEqual(p.relative_to('a/'), P('b')) self.assertEqual(p.relative_to(P('a/b')), P()) self.assertEqual(p.relative_to('a/b'), P()) # With several args. self.assertEqual(p.relative_to('a', 'b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('c')) self.assertRaises(ValueError, p.relative_to, P('a/b/c')) self.assertRaises(ValueError, p.relative_to, P('a/c')) self.assertRaises(ValueError, p.relative_to, P('/a')) p = P('/a/b') self.assertEqual(p.relative_to(P('/')), P('a/b')) self.assertEqual(p.relative_to('/'), P('a/b')) self.assertEqual(p.relative_to(P('/a')), P('b')) self.assertEqual(p.relative_to('/a'), P('b')) self.assertEqual(p.relative_to('/a/'), P('b')) self.assertEqual(p.relative_to(P('/a/b')), P()) self.assertEqual(p.relative_to('/a/b'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/c')) self.assertRaises(ValueError, p.relative_to, P('/a/b/c')) self.assertRaises(ValueError, p.relative_to, P('/a/c')) self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('a')) def test_is_relative_to_common(self): P = self.cls p = P('a/b') self.assertRaises(TypeError, p.is_relative_to) self.assertRaises(TypeError, p.is_relative_to, b'a') self.assertTrue(p.is_relative_to(P())) self.assertTrue(p.is_relative_to('')) self.assertTrue(p.is_relative_to(P('a'))) self.assertTrue(p.is_relative_to('a/')) self.assertTrue(p.is_relative_to(P('a/b'))) self.assertTrue(p.is_relative_to('a/b')) # With several args. self.assertTrue(p.is_relative_to('a', 'b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('c'))) self.assertFalse(p.is_relative_to(P('a/b/c'))) self.assertFalse(p.is_relative_to(P('a/c'))) self.assertFalse(p.is_relative_to(P('/a'))) p = P('/a/b') self.assertTrue(p.is_relative_to(P('/'))) self.assertTrue(p.is_relative_to('/')) self.assertTrue(p.is_relative_to(P('/a'))) self.assertTrue(p.is_relative_to('/a')) self.assertTrue(p.is_relative_to('/a/')) self.assertTrue(p.is_relative_to(P('/a/b'))) self.assertTrue(p.is_relative_to('/a/b')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/c'))) self.assertFalse(p.is_relative_to(P('/a/b/c'))) self.assertFalse(p.is_relative_to(P('/a/c'))) self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) def test_pickling_common(self): P = self.cls p = P('/a/b') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertIs(pp.__class__, p.__class__) self.assertEqual(pp, p) self.assertEqual(hash(pp), hash(p)) self.assertEqual(str(pp), str(p)) class PurePosixPathTest(_BasePurePathTest): cls = pathlib.PurePosixPath def test_root(self): P = self.cls self.assertEqual(P('/a/b').root, '/') self.assertEqual(P('///a/b').root, '/') # POSIX special case for two leading slashes. self.assertEqual(P('//a/b').root, '//') def test_eq(self): P = self.cls self.assertNotEqual(P('a/b'), P('A/b')) self.assertEqual(P('/a'), P('///a')) self.assertNotEqual(P('/a'), P('//a')) def test_as_uri(self): P = self.cls self.assertEqual(P('/').as_uri(), 'file:///') self.assertEqual(P('/a/b.c').as_uri(), 'file:///a/b.c') self.assertEqual(P('/a/b%#c').as_uri(), 'file:///a/b%25%23c') def test_as_uri_non_ascii(self): from urllib.parse import quote_from_bytes P = self.cls try: os.fsencode('\xe9') except UnicodeEncodeError: self.skipTest("\\xe9 cannot be encoded to the filesystem encoding") self.assertEqual(P('/a/b\xe9').as_uri(), 'file:///a/b' + quote_from_bytes(os.fsencode('\xe9'))) def test_match(self): P = self.cls self.assertFalse(P('A.py').match('a.PY')) def test_is_absolute(self): P = self.cls self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertTrue(P('/').is_absolute()) self.assertTrue(P('/a').is_absolute()) self.assertTrue(P('/a/b/').is_absolute()) self.assertTrue(P('//a').is_absolute()) self.assertTrue(P('//a/b').is_absolute()) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved()) def test_join(self): P = self.cls p = P('//a') pp = p.joinpath('b') self.assertEqual(pp, P('//a/b')) pp = P('/a').joinpath('//c') self.assertEqual(pp, P('//c')) pp = P('//a').joinpath('/c') self.assertEqual(pp, P('/c')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) class PureWindowsPathTest(_BasePurePathTest): cls = pathlib.PureWindowsPath equivalences = _BasePurePathTest.equivalences.copy() equivalences.update({ 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ], 'c:/a': [ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'), ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'), ], '//a/b/': [ ('//a/b',) ], '//a/b/c': [ ('//a/b', 'c'), ('//a/b/', 'c'), ], }) def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') p = self.cls('c:/a/b/c') self.assertEqual(str(p), 'c:\\a\\b\\c') p = self.cls('//a/b') self.assertEqual(str(p), '\\\\a\\b\\') p = self.cls('//a/b/c') self.assertEqual(str(p), '\\\\a\\b\\c') p = self.cls('//a/b/c/d') self.assertEqual(str(p), '\\\\a\\b\\c\\d') def test_str_subclass(self): self._check_str_subclass('c:') self._check_str_subclass('c:a') self._check_str_subclass('c:a\\b.txt') self._check_str_subclass('c:\\') self._check_str_subclass('c:\\a') self._check_str_subclass('c:\\a\\b.txt') self._check_str_subclass('\\\\some\\share') self._check_str_subclass('\\\\some\\share\\a') self._check_str_subclass('\\\\some\\share\\a\\b.txt') def test_eq(self): P = self.cls self.assertEqual(P('c:a/b'), P('c:a/b')) self.assertEqual(P('c:a/b'), P('c:', 'a', 'b')) self.assertNotEqual(P('c:a/b'), P('d:a/b')) self.assertNotEqual(P('c:a/b'), P('c:/a/b')) self.assertNotEqual(P('/a/b'), P('c:/a/b')) # Case-insensitivity. self.assertEqual(P('a/B'), P('A/b')) self.assertEqual(P('C:a/B'), P('c:A/b')) self.assertEqual(P('//Some/SHARE/a/B'), P('//somE/share/A/b')) def test_as_uri(self): P = self.cls with self.assertRaises(ValueError): P('/a/b').as_uri() with self.assertRaises(ValueError): P('c:a/b').as_uri() self.assertEqual(P('c:/').as_uri(), 'file:///c:/') self.assertEqual(P('c:/a/b.c').as_uri(), 'file:///c:/a/b.c') self.assertEqual(P('c:/a/b%#c').as_uri(), 'file:///c:/a/b%25%23c') self.assertEqual(P('c:/a/b\xe9').as_uri(), 'file:///c:/a/b%C3%A9') self.assertEqual(P('//some/share/').as_uri(), 'file://some/share/') self.assertEqual(P('//some/share/a/b.c').as_uri(), 'file://some/share/a/b.c') self.assertEqual(P('//some/share/a/b%#c\xe9').as_uri(), 'file://some/share/a/b%25%23c%C3%A9') def test_match_common(self): P = self.cls # Absolute patterns. self.assertTrue(P('c:/b.py').match('/*.py')) self.assertTrue(P('c:/b.py').match('c:*.py')) self.assertTrue(P('c:/b.py').match('c:/*.py')) self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive self.assertFalse(P('b.py').match('/*.py')) self.assertFalse(P('b.py').match('c:*.py')) self.assertFalse(P('b.py').match('c:/*.py')) self.assertFalse(P('c:b.py').match('/*.py')) self.assertFalse(P('c:b.py').match('c:/*.py')) self.assertFalse(P('/b.py').match('c:*.py')) self.assertFalse(P('/b.py').match('c:/*.py')) # UNC patterns. self.assertTrue(P('//some/share/a.py').match('/*.py')) self.assertTrue(P('//some/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//other/share/a.py').match('//some/share/*.py')) self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py')) # Case-insensitivity. self.assertTrue(P('B.py').match('b.PY')) self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY')) self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY')) def test_ordering_common(self): # Case-insensitivity. def assertOrderedEqual(a, b): self.assertLessEqual(a, b) self.assertGreaterEqual(b, a) P = self.cls p = P('c:A/b') q = P('C:a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) p = P('//some/Share/A/b') q = P('//Some/SHARE/a/B') assertOrderedEqual(p, q) self.assertFalse(p < q) self.assertFalse(p > q) def test_parts(self): P = self.cls p = P('c:a/b') parts = p.parts self.assertEqual(parts, ('c:', 'a', 'b')) p = P('c:/a/b') parts = p.parts self.assertEqual(parts, ('c:\\', 'a', 'b')) p = P('//a/b/c/d') parts = p.parts self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd')) def test_parent(self): # Anchored P = self.cls p = P('z:a/b/c') self.assertEqual(p.parent, P('z:a/b')) self.assertEqual(p.parent.parent, P('z:a')) self.assertEqual(p.parent.parent.parent, P('z:')) self.assertEqual(p.parent.parent.parent.parent, P('z:')) p = P('z:/a/b/c') self.assertEqual(p.parent, P('z:/a/b')) self.assertEqual(p.parent.parent, P('z:/a')) self.assertEqual(p.parent.parent.parent, P('z:/')) self.assertEqual(p.parent.parent.parent.parent, P('z:/')) p = P('//a/b/c/d') self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) def test_parents(self): # Anchored P = self.cls p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:a')) self.assertEqual(par[1], P('z:')) self.assertEqual(list(par), [P('z:a'), P('z:')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:/a')) self.assertEqual(par[1], P('z:/')) self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('//a/b/c/d') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('//a/b/c')) self.assertEqual(par[1], P('//a/b')) self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')]) with self.assertRaises(IndexError): par[2] def test_drive(self): P = self.cls self.assertEqual(P('c:').drive, 'c:') self.assertEqual(P('c:a/b').drive, 'c:') self.assertEqual(P('c:/').drive, 'c:') self.assertEqual(P('c:/a/b/').drive, 'c:') self.assertEqual(P('//a/b').drive, '\\\\a\\b') self.assertEqual(P('//a/b/').drive, '\\\\a\\b') self.assertEqual(P('//a/b/c/d').drive, '\\\\a\\b') def test_root(self): P = self.cls self.assertEqual(P('c:').root, '') self.assertEqual(P('c:a/b').root, '') self.assertEqual(P('c:/').root, '\\') self.assertEqual(P('c:/a/b/').root, '\\') self.assertEqual(P('//a/b').root, '\\') self.assertEqual(P('//a/b/').root, '\\') self.assertEqual(P('//a/b/c/d').root, '\\') def test_anchor(self): P = self.cls self.assertEqual(P('c:').anchor, 'c:') self.assertEqual(P('c:a/b').anchor, 'c:') self.assertEqual(P('c:/').anchor, 'c:\\') self.assertEqual(P('c:/a/b/').anchor, 'c:\\') self.assertEqual(P('//a/b').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/').anchor, '\\\\a\\b\\') self.assertEqual(P('//a/b/c/d').anchor, '\\\\a\\b\\') def test_name(self): P = self.cls self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') self.assertEqual(P('c:/a/.hgrc').suffix, '') self.assertEqual(P('c:a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) self.assertEqual(P('c:/a/.hgrc').suffixes, []) self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('//My.py/Share.php').suffixes, []) self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) def test_stem(self): P = self.cls self.assertEqual(P('c:').stem, '') self.assertEqual(P('c:.').stem, '') self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') def test_with_name(self): P = self.cls self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_name, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_name, '//My/Share') def test_with_stem(self): P = self.cls self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:e') self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share') def test_with_suffix(self): P = self.cls self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d') def test_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:foO')), P('Bar')) self.assertEqual(p.relative_to('c:foO'), P('Bar')) self.assertEqual(p.relative_to('c:foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:foO/baR')), P()) self.assertEqual(p.relative_to('c:foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P()) self.assertRaises(ValueError, p.relative_to, '') self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('Foo')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo/Baz')) p = P('C:/Foo/Bar') self.assertEqual(p.relative_to(P('c:')), P('/Foo/Bar')) self.assertEqual(p.relative_to('c:'), P('/Foo/Bar')) self.assertEqual(str(p.relative_to(P('c:'))), '\\Foo\\Bar') self.assertEqual(str(p.relative_to('c:')), '\\Foo\\Bar') self.assertEqual(p.relative_to(P('c:/')), P('Foo/Bar')) self.assertEqual(p.relative_to('c:/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('c:/foO')), P('Bar')) self.assertEqual(p.relative_to('c:/foO'), P('Bar')) self.assertEqual(p.relative_to('c:/foO/'), P('Bar')) self.assertEqual(p.relative_to(P('c:/foO/baR')), P()) self.assertEqual(p.relative_to('c:/foO/baR'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('C:/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Bar/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:/Foo/Baz')) self.assertRaises(ValueError, p.relative_to, P('C:Foo')) self.assertRaises(ValueError, p.relative_to, P('d:')) self.assertRaises(ValueError, p.relative_to, P('d:/')) self.assertRaises(ValueError, p.relative_to, P('/')) self.assertRaises(ValueError, p.relative_to, P('/Foo')) self.assertRaises(ValueError, p.relative_to, P('//C/Foo')) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertEqual(p.relative_to(P('//sErver/sHare')), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare'), P('Foo/Bar')) self.assertEqual(p.relative_to('//sErver/sHare/'), P('Foo/Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo')), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo'), P('Bar')) self.assertEqual(p.relative_to('//sErver/sHare/Foo/'), P('Bar')) self.assertEqual(p.relative_to(P('//sErver/sHare/Foo/Bar')), P()) self.assertEqual(p.relative_to('//sErver/sHare/Foo/Bar'), P()) # Unrelated paths. self.assertRaises(ValueError, p.relative_to, P('/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('c:/Server/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//z/Share/Foo')) self.assertRaises(ValueError, p.relative_to, P('//Server/z/Foo')) def test_is_relative_to(self): P = self.cls p = P('C:Foo/Bar') self.assertTrue(p.is_relative_to(P('c:'))) self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:foO'))) self.assertTrue(p.is_relative_to('c:foO')) self.assertTrue(p.is_relative_to('c:foO/')) self.assertTrue(p.is_relative_to(P('c:foO/baR'))) self.assertTrue(p.is_relative_to('c:foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P())) self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('Foo'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('C:/Foo'))) self.assertFalse(p.is_relative_to(P('C:Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo/Baz'))) p = P('C:/Foo/Bar') self.assertTrue(p.is_relative_to('c:')) self.assertTrue(p.is_relative_to(P('c:/'))) self.assertTrue(p.is_relative_to(P('c:/foO'))) self.assertTrue(p.is_relative_to('c:/foO/')) self.assertTrue(p.is_relative_to(P('c:/foO/baR'))) self.assertTrue(p.is_relative_to('c:/foO/baR')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('C:/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Bar/Baz'))) self.assertFalse(p.is_relative_to(P('C:/Foo/Baz'))) self.assertFalse(p.is_relative_to(P('C:Foo'))) self.assertFalse(p.is_relative_to(P('d:'))) self.assertFalse(p.is_relative_to(P('d:/'))) self.assertFalse(p.is_relative_to(P('/'))) self.assertFalse(p.is_relative_to(P('/Foo'))) self.assertFalse(p.is_relative_to(P('//C/Foo'))) # UNC paths. p = P('//Server/Share/Foo/Bar') self.assertTrue(p.is_relative_to(P('//sErver/sHare'))) self.assertTrue(p.is_relative_to('//sErver/sHare')) self.assertTrue(p.is_relative_to('//sErver/sHare/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo')) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/')) self.assertTrue(p.is_relative_to(P('//sErver/sHare/Foo/Bar'))) self.assertTrue(p.is_relative_to('//sErver/sHare/Foo/Bar')) # Unrelated paths. self.assertFalse(p.is_relative_to(P('/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('c:/Server/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//z/Share/Foo'))) self.assertFalse(p.is_relative_to(P('//Server/z/Foo'))) def test_is_absolute(self): P = self.cls # Under NT, only paths with both a drive and a root are absolute. self.assertFalse(P().is_absolute()) self.assertFalse(P('a').is_absolute()) self.assertFalse(P('a/b/').is_absolute()) self.assertFalse(P('/').is_absolute()) self.assertFalse(P('/a').is_absolute()) self.assertFalse(P('/a/b/').is_absolute()) self.assertFalse(P('c:').is_absolute()) self.assertFalse(P('c:a').is_absolute()) self.assertFalse(P('c:a/b/').is_absolute()) self.assertTrue(P('c:/').is_absolute()) self.assertTrue(P('c:/a').is_absolute()) self.assertTrue(P('c:/a/b/').is_absolute()) # UNC paths are absolute by definition. self.assertTrue(P('//a/b').is_absolute()) self.assertTrue(P('//a/b/').is_absolute()) self.assertTrue(P('//a/b/c').is_absolute()) self.assertTrue(P('//a/b/c/d').is_absolute()) def test_join(self): P = self.cls p = P('C:/a/b') pp = p.joinpath('x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. pp = p.joinpath('D:x/y') self.assertEqual(pp, P('D:x/y')) pp = p.joinpath('D:/x/y') self.assertEqual(pp, P('D:/x/y')) pp = p.joinpath('//host/share/x/y') self.assertEqual(pp, P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. pp = p.joinpath('c:x/y') self.assertEqual(pp, P('C:/a/b/x/y')) pp = p.joinpath('c:/x/y') self.assertEqual(pp, P('C:/x/y')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('C:/a/b') self.assertEqual(p / 'x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'x' / 'y', P('C:/a/b/x/y')) self.assertEqual(p / '/x/y', P('C:/x/y')) self.assertEqual(p / '/x' / 'y', P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. self.assertEqual(p / 'D:x/y', P('D:x/y')) self.assertEqual(p / 'D:' / 'x/y', P('D:x/y')) self.assertEqual(p / 'D:/x/y', P('D:/x/y')) self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y')) self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'c:/x/y', P('C:/x/y')) def test_is_reserved(self): P = self.cls self.assertIs(False, P('').is_reserved()) self.assertIs(False, P('/').is_reserved()) self.assertIs(False, P('/foo/bar').is_reserved()) # UNC paths are never reserved. self.assertIs(False, P('//my/share/nul/con/aux').is_reserved()) # Case-insensitive DOS-device names are reserved. self.assertIs(True, P('nul').is_reserved()) self.assertIs(True, P('aux').is_reserved()) self.assertIs(True, P('prn').is_reserved()) self.assertIs(True, P('con').is_reserved()) self.assertIs(True, P('conin$').is_reserved()) self.assertIs(True, P('conout$').is_reserved()) # COM/LPT + 1-9 or + superscript 1-3 are reserved. self.assertIs(True, P('COM1').is_reserved()) self.assertIs(True, P('LPT9').is_reserved()) self.assertIs(True, P('com\xb9').is_reserved()) self.assertIs(True, P('com\xb2').is_reserved()) self.assertIs(True, P('lpt\xb3').is_reserved()) # DOS-device name mataching ignores characters after a dot or # a colon and also ignores trailing spaces. self.assertIs(True, P('NUL.txt').is_reserved()) self.assertIs(True, P('PRN ').is_reserved()) self.assertIs(True, P('AUX .txt').is_reserved()) self.assertIs(True, P('COM1:bar').is_reserved()) self.assertIs(True, P('LPT9 :bar').is_reserved()) # DOS-device names are only matched at the beginning # of a path component. self.assertIs(False, P('bar.com9').is_reserved()) self.assertIs(False, P('bar.lpt9').is_reserved()) # Only the last path component matters. self.assertIs(True, P('c:/baz/con/NUL').is_reserved()) self.assertIs(False, P('c:/NUL/con/baz').is_reserved()) class PurePathTest(_BasePurePathTest): cls = pathlib.PurePath def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath) def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') self.assertNotEqual(p, q) def test_different_flavours_unordered(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') with self.assertRaises(TypeError): p < q with self.assertRaises(TypeError): p <= q with self.assertRaises(TypeError): p > q with self.assertRaises(TypeError): p >= q # # Tests for the concrete classes. # # Make sure any symbolic links in the base test path are resolved. BASE = os.path.realpath(TESTFN) join = lambda *x: os.path.join(BASE, *x) rel_join = lambda *x: os.path.join(TESTFN, *x) only_nt = unittest.skipIf(os.name != 'nt', 'test requires a Windows-compatible system') only_posix = unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system') @only_posix class PosixPathAsPureTest(PurePosixPathTest, unittest.TestCase): cls = PosixUPath @only_nt class WindowsPathAsPureTest(PureWindowsPathTest, unittest.TestCase): cls = WindowsUPath def test_owner(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').owner() def test_group(self): P = self.cls with self.assertRaises(NotImplementedError): P('c:/').group() class _BasePathTest(object): """Tests for the FS-accessing functionalities of the Path classes.""" # (BASE) # | # |-- brokenLink -> non-existing # |-- dirA # | `-- linkC -> ../dirB # |-- dirB # | |-- fileB # | `-- linkD -> ../dirB # |-- dirC # | |-- dirD # | | `-- fileD # | `-- fileC # |-- dirE # No permissions # |-- fileA # |-- linkA -> fileA # |-- linkB -> dirB # `-- brokenLinkLoop -> brokenLinkLoop # def setUp(self): def cleanup(): os.chmod(join('dirE'), 0o777) support.rmtree(BASE) self.addCleanup(cleanup) os.mkdir(BASE) os.mkdir(join('dirA')) os.mkdir(join('dirB')) os.mkdir(join('dirC')) os.mkdir(join('dirC', 'dirD')) os.mkdir(join('dirE')) with open(join('fileA'), 'wb') as f: f.write(b"this is file A\n") with open(join('dirB', 'fileB'), 'wb') as f: f.write(b"this is file B\n") with open(join('dirC', 'fileC'), 'wb') as f: f.write(b"this is file C\n") with open(join('dirC', 'dirD', 'fileD'), 'wb') as f: f.write(b"this is file D\n") os.chmod(join('dirE'), 0) if support.can_symlink(): # Relative symlinks. os.symlink('fileA', join('linkA')) os.symlink('non-existing', join('brokenLink')) self.dirlink('dirB', join('linkB')) self.dirlink(os.path.join('..', 'dirB'), join('dirA', 'linkC')) # This one goes upwards, creating a loop. self.dirlink(os.path.join('..', 'dirB'), join('dirB', 'linkD')) # Broken symlink (pointing to itself). os.symlink('brokenLinkLoop', join('brokenLinkLoop')) if os.name == 'nt': # Workaround for http://bugs.python.org/issue13772. def dirlink(self, src, dest): os.symlink(src, dest, target_is_directory=True) else: def dirlink(self, src, dest): os.symlink(src, dest) def assertSame(self, path_a, path_b): self.assertTrue(os.path.samefile(str(path_a), str(path_b)), "%r and %r don't point to the same file" % (path_a, path_b)) def assertFileNotFound(self, func, *args, **kwargs): with self.assertRaises(FileNotFoundError) as cm: func(*args, **kwargs) self.assertEqual(cm.exception.errno, errno.ENOENT) def assertEqualNormCase(self, path_a, path_b): self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b)) def _test_cwd(self, p): q = self.cls(os.getcwd()) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_cwd(self): p = self.cls.cwd() self._test_cwd(p) def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) self.assertEqualNormCase(str(p), str(q)) self.assertIs(type(p), type(q)) self.assertTrue(p.is_absolute()) def test_home(self): with support.EnvironmentVarGuard() as env: self._test_home(self.cls.home()) env.clear() env['USERPROFILE'] = os.path.join(BASE, 'userprofile') self._test_home(self.cls.home()) # bpo-38883: ignore `HOME` when set on windows env['HOME'] = os.path.join(BASE, 'home') self._test_home(self.cls.home()) def test_samefile(self): fileA_path = os.path.join(BASE, 'fileA') fileB_path = os.path.join(BASE, 'dirB', 'fileB') p = self.cls(fileA_path) pp = self.cls(fileA_path) q = self.cls(fileB_path) self.assertTrue(p.samefile(fileA_path)) self.assertTrue(p.samefile(pp)) self.assertFalse(p.samefile(fileB_path)) self.assertFalse(p.samefile(q)) # Test the non-existent file case non_existent = os.path.join(BASE, 'foo') r = self.cls(non_existent) self.assertRaises(FileNotFoundError, p.samefile, r) self.assertRaises(FileNotFoundError, p.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, p) self.assertRaises(FileNotFoundError, r.samefile, non_existent) self.assertRaises(FileNotFoundError, r.samefile, r) self.assertRaises(FileNotFoundError, r.samefile, non_existent) def test_empty_path(self): # The empty path points to '.' p = self.cls('') self.assertEqual(p.stat(), os.stat('.')) def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') self.assertEqual(p.expanduser(), p) p = P('../~') self.assertEqual(p.expanduser(), p) p = P(P('').absolute().anchor) / '~' self.assertEqual(p.expanduser(), p) def test_exists(self): P = self.cls p = P(BASE) self.assertIs(True, p.exists()) self.assertIs(True, (p / 'dirA').exists()) self.assertIs(True, (p / 'fileA').exists()) self.assertIs(False, (p / 'fileA' / 'bah').exists()) if support.can_symlink(): self.assertIs(True, (p / 'linkA').exists()) self.assertIs(True, (p / 'linkB').exists()) self.assertIs(True, (p / 'linkB' / 'fileB').exists()) self.assertIs(False, (p / 'linkA' / 'bah').exists()) self.assertIs(False, (p / 'foo').exists()) self.assertIs(False, P('/xyzzy').exists()) self.assertIs(False, P(BASE + '\udfff').exists()) self.assertIs(False, P(BASE + '\x00').exists()) def test_open_common(self): p = self.cls(BASE) with (p / 'fileA').open('r') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), "this is file A\n") with (p / 'fileA').open('rb') as f: self.assertIsInstance(f, io.BufferedIOBase) self.assertEqual(f.read().strip(), b"this is file A") with (p / 'fileA').open('rb', buffering=0) as f: self.assertIsInstance(f, io.RawIOBase) self.assertEqual(f.read().strip(), b"this is file A") def test_read_write_bytes(self): p = self.cls(BASE) (p / 'fileA').write_bytes(b'abcdefg') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') # Check that trying to write str does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr') self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg') def test_read_write_text(self): p = self.cls(BASE) (p / 'fileA').write_text('äbcdefg', encoding='latin-1') self.assertEqual((p / 'fileA').read_text( encoding='utf-8', errors='ignore'), 'bcdefg') # Check that trying to write bytes does not truncate the file. self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes') self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg') def test_iterdir(self): P = self.cls p = P(BASE) it = p.iterdir() paths = set(it) expected = ['dirA', 'dirB', 'dirC', 'dirE', 'fileA'] if support.can_symlink(): expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop'] self.assertEqual(paths, { P(BASE, q) for q in expected }) @support.skip_unless_symlink def test_iterdir_symlink(self): # __iter__ on a symlink to a directory. P = self.cls p = P(BASE, 'linkB') paths = set(p.iterdir()) expected = { P(BASE, 'linkB', q) for q in ['fileB', 'linkD'] } self.assertEqual(paths, expected) def test_iterdir_nodir(self): # __iter__ on something that is not a directory. p = self.cls(BASE, 'fileA') with self.assertRaises(OSError) as cm: next(p.iterdir()) # ENOENT or EINVAL under Windows, ENOTDIR otherwise # (see issue #12802). self.assertIn(cm.exception.errno, (errno.ENOTDIR, errno.ENOENT, errno.EINVAL)) def test_glob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.glob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.glob("fileB"), []) _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not support.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) else: _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) if not support.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not support.can_symlink(): _check(p.glob("*/fileB"), ['dirB/fileB']) else: _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) def test_rglob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) it = p.rglob("fileA") self.assertIsInstance(it, collections.abc.Iterator) _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) if not support.can_symlink(): _check(p.rglob("*/fileB"), ["dirB/fileB"]) else: _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) p = P(BASE, "dirC") _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) @support.skip_unless_symlink def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) given = set(p.rglob('*')) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', 'dirC', 'dirC/dirD', 'dirC/dirD/fileD', 'dirC/fileC', 'dirE', 'fileA', 'linkA', 'linkB', 'brokenLinkLoop', } self.assertEqual(given, {p / x for x in expect}) def test_glob_many_open_files(self): depth = 30 P = self.cls base = P(BASE) / 'deep' p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) iters = [base.glob(pattern) for j in range(100)] for it in iters: self.assertEqual(next(it), p) iters = [base.rglob('d') for j in range(100)] p = base for i in range(depth): p = p / 'd' for it in iters: self.assertEqual(next(it), p) def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) self.assertEqual(set(p.glob("../xyzzy")), set()) @support.skip_unless_symlink def test_glob_permissions(self): # See bpo-38894 P = self.cls base = P(BASE) / 'permissions' base.mkdir() file1 = base / "file1" file1.touch() file2 = base / "file2" file2.touch() subdir = base / "subdir" file3 = base / "file3" file3.symlink_to(subdir / "other") # Patching is needed to avoid relying on the filesystem # to return the order of the files as the error will not # happen if the symlink is the last item. with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 3) subdir.mkdir() with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 4) subdir.chmod(000) with mock.patch("os.scandir") as scandir: scandir.return_value = sorted(os.scandir(base)) self.assertEqual(len(set(base.glob("*"))), 4) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) self.assertEqual(q, expected) # This can be used to check both relative and absolute resolutions. _check_resolve_relative = _check_resolve_absolute = _check_resolve @support.skip_unless_symlink def test_resolve_common(self): P = self.cls p = P(BASE, 'foo') with self.assertRaises(OSError) as cm: p.resolve(strict=True) self.assertEqual(cm.exception.errno, errno.ENOENT) # Non-strict self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo')) p = P(BASE, 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.join(BASE, 'foo', 'in', 'spam')) p = P(BASE, '..', 'foo', 'in', 'spam') self.assertEqualNormCase(str(p.resolve(strict=False)), os.path.abspath(os.path.join('foo', 'in', 'spam'))) # These are all relative symlinks. p = P(BASE, 'dirB', 'fileB') self._check_resolve_relative(p, p) p = P(BASE, 'linkA') self._check_resolve_relative(p, P(BASE, 'fileA')) p = P(BASE, 'dirA', 'linkC', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) p = P(BASE, 'dirB', 'linkD', 'fileB') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(BASE, 'dirA', 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) # Now create absolute symlinks. d = support._longpath(tempfile.mkdtemp(suffix='-dirD', dir=os.getcwd())) self.addCleanup(support.rmtree, d) os.symlink(os.path.join(d), join('dirA', 'linkX')) os.symlink(join('dirB'), os.path.join(d, 'linkY')) p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB') self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB')) # Non-strict p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam') self._check_resolve_relative(p, P(BASE, 'dirB', 'foo', 'in', 'spam'), False) p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam') if os.name == 'nt': # In Windows, if linkY points to dirB, 'dirA\linkY\..' # resolves to 'dirA' without resolving linkY first. self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False) else: # In Posix, if linkY points to dirB, 'dirA/linkY/..' # resolves to 'dirB/..' first before resolving to parent of dirB. self._check_resolve_relative(p, P(BASE, 'foo', 'in', 'spam'), False) @support.skip_unless_symlink def test_resolve_dot(self): # See https://bitbucket.org/pitrou/pathlib/issue/9/pathresolve-fails-on-complex-symlinks p = self.cls(BASE) self.dirlink('.', join('0')) self.dirlink(os.path.join('0', '0'), join('1')) self.dirlink(os.path.join('1', '1'), join('2')) q = p / '2' self.assertEqual(q.resolve(strict=True), p) r = q / '3' / '4' self.assertRaises(FileNotFoundError, r.resolve, strict=True) # Non-strict self.assertEqual(r.resolve(strict=False), p / '3' / '4') def test_with(self): p = self.cls(BASE) it = p.iterdir() it2 = p.iterdir() next(it2) with p: pass # Using a path as a context manager is a no-op, thus the following # operations should still succeed after the context manage exits. next(it) next(it2) p.exists() p.resolve() p.absolute() with p: pass def test_chmod(self): p = self.cls(BASE) / 'fileA' mode = p.stat().st_mode # Clear writable bit. new_mode = mode & ~0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # Set writable bit. new_mode = mode | 0o222 p.chmod(new_mode) self.assertEqual(p.stat().st_mode, new_mode) # XXX also need a test for lchmod. def test_stat(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(p.stat(), st) # Change file mode by flipping write bit. p.chmod(st.st_mode ^ 0o222) self.addCleanup(p.chmod, st.st_mode) self.assertNotEqual(p.stat(), st) @support.skip_unless_symlink def test_lstat(self): p = self.cls(BASE)/ 'linkA' st = p.stat() self.assertNotEqual(st, p.lstat()) def test_lstat_nosymlink(self): p = self.cls(BASE) / 'fileA' st = p.stat() self.assertEqual(st, p.lstat()) @unittest.skipUnless(pwd, "the pwd module is needed for this test") def test_owner(self): p = self.cls(BASE) / 'fileA' uid = p.stat().st_uid try: name = pwd.getpwuid(uid).pw_name except KeyError: self.skipTest( "user %d doesn't have an entry in the system database" % uid) self.assertEqual(name, p.owner()) @unittest.skipUnless(grp, "the grp module is needed for this test") def test_group(self): p = self.cls(BASE) / 'fileA' gid = p.stat().st_gid try: name = grp.getgrgid(gid).gr_name except KeyError: self.skipTest( "group %d doesn't have an entry in the system database" % gid) self.assertEqual(name, p.group()) def test_unlink(self): p = self.cls(BASE) / 'fileA' p.unlink() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) def test_unlink_missing_ok(self): p = self.cls(BASE) / 'fileAAA' self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self): p = self.cls(BASE) / 'dirA' for q in p.iterdir(): q.unlink() p.rmdir() self.assertFileNotFound(p.stat) self.assertFileNotFound(p.unlink) @unittest.skipUnless(hasattr(os, "link"), "os.link() is not present") def test_link_to(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # linking to another path. q = P / 'dirA' / 'fileAA' try: p.link_to(q) except PermissionError as e: self.skipTest('os.link(): %s' % e) self.assertEqual(q.stat().st_size, size) self.assertEqual(os.path.samefile(p, q), True) self.assertTrue(p.stat) # Linking to a str of a relative path. r = rel_join('fileAAA') q.link_to(r) self.assertEqual(os.stat(r).st_size, size) self.assertTrue(q.stat) @unittest.skipIf(hasattr(os, "link"), "os.link() is present") def test_link_to_not_implemented(self): P = self.cls(BASE) p = P / 'fileA' # linking to another path. q = P / 'dirA' / 'fileAA' with self.assertRaises(NotImplementedError): p.link_to(q) def test_rename(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Renaming to another path. q = P / 'dirA' / 'fileAA' renamed_p = p.rename(q) self.assertEqual(renamed_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Renaming to a str of a relative path. r = rel_join('fileAAA') renamed_q = q.rename(r) self.assertEqual(renamed_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) def test_replace(self): P = self.cls(BASE) p = P / 'fileA' size = p.stat().st_size # Replacing a non-existing path. q = P / 'dirA' / 'fileAA' replaced_p = p.replace(q) self.assertEqual(replaced_p, q) self.assertEqual(q.stat().st_size, size) self.assertFileNotFound(p.stat) # Replacing another (existing) path. r = rel_join('dirB', 'fileB') replaced_q = q.replace(r) self.assertEqual(replaced_q, self.cls(r)) self.assertEqual(os.stat(r).st_size, size) self.assertFileNotFound(q.stat) @support.skip_unless_symlink def test_readlink(self): P = self.cls(BASE) self.assertEqual((P / 'linkA').readlink(), self.cls('fileA')) self.assertEqual((P / 'brokenLink').readlink(), self.cls('non-existing')) self.assertEqual((P / 'linkB').readlink(), self.cls('dirB')) with self.assertRaises(OSError): (P / 'fileA').readlink() def test_touch_common(self): P = self.cls(BASE) p = P / 'newfileA' self.assertFalse(p.exists()) p.touch() self.assertTrue(p.exists()) st = p.stat() old_mtime = st.st_mtime old_mtime_ns = st.st_mtime_ns # Rewind the mtime sufficiently far in the past to work around # filesystem-specific timestamp granularity. os.utime(str(p), (old_mtime - 10, old_mtime - 10)) # The file mtime should be refreshed by calling touch() again. p.touch() st = p.stat() self.assertGreaterEqual(st.st_mtime_ns, old_mtime_ns) self.assertGreaterEqual(st.st_mtime, old_mtime) # Now with exist_ok=False. p = P / 'newfileB' self.assertFalse(p.exists()) p.touch(mode=0o700, exist_ok=False) self.assertTrue(p.exists()) self.assertRaises(OSError, p.touch, exist_ok=False) def test_touch_nochange(self): P = self.cls(BASE) p = P / 'fileA' p.touch() with p.open('rb') as f: self.assertEqual(f.read().strip(), b"this is file A") def test_mkdir(self): P = self.cls(BASE) p = P / 'newdirA' self.assertFalse(p.exists()) p.mkdir() self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_parents(self): # Creating a chain of directories. p = self.cls(BASE, 'newdirB', 'newdirC') self.assertFalse(p.exists()) with self.assertRaises(OSError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.ENOENT) p.mkdir(parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(OSError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) # Test `mode` arg. mode = stat.S_IMODE(p.stat().st_mode) # Default mode. p = self.cls(BASE, 'newdirD', 'newdirE') p.mkdir(0o555, parents=True) self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) if os.name != 'nt': # The directory's permissions follow the mode argument. self.assertEqual(stat.S_IMODE(p.stat().st_mode), 0o7555 & mode) # The parent's permissions follow the default process settings. self.assertEqual(stat.S_IMODE(p.parent.stat().st_mode), mode) def test_mkdir_exist_ok(self): p = self.cls(BASE, 'dirB') st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) self.assertTrue(p.is_dir()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_with_parent(self): p = self.cls(BASE, 'dirC') self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) p = p / 'newdirC' p.mkdir(parents=True) st_ctime_first = p.stat().st_ctime self.assertTrue(p.exists()) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) p.mkdir(parents=True, exist_ok=True) self.assertTrue(p.exists()) self.assertEqual(p.stat().st_ctime, st_ctime_first) def test_mkdir_exist_ok_root(self): # Issue #25803: A drive root could raise PermissionError on Windows. self.cls('/').resolve().mkdir(exist_ok=True) self.cls('/').resolve().mkdir(parents=True, exist_ok=True) @only_nt # XXX: not sure how to test this on POSIX. def test_mkdir_with_unknown_drive(self): for d in 'ZYXWVUTSRQPONMLKJIHGFEDCBA': p = self.cls(d + ':\\') if not p.is_dir(): break else: self.skipTest("cannot find a drive that doesn't exist") with self.assertRaises(OSError): (p / 'child' / 'path').mkdir(parents=True) def test_mkdir_with_child_file(self): p = self.cls(BASE, 'dirB', 'fileB') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True) self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(parents=True, exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_no_parents_file(self): p = self.cls(BASE, 'fileA') self.assertTrue(p.exists()) # An exception is raised when the last path component is an existing # regular file, regardless of whether exist_ok is true or not. with self.assertRaises(FileExistsError) as cm: p.mkdir() self.assertEqual(cm.exception.errno, errno.EEXIST) with self.assertRaises(FileExistsError) as cm: p.mkdir(exist_ok=True) self.assertEqual(cm.exception.errno, errno.EEXIST) def test_mkdir_concurrent_parent_creation(self): for pattern_num in range(32): p = self.cls(BASE, 'dirCPC%d' % pattern_num) self.assertFalse(p.exists()) def my_mkdir(path, mode=0o777): path = str(path) # Emulate another process that would create the directory # just before we try to create it ourselves. We do it # in all possible pattern combinations, assuming that this # function is called at most 5 times (dirCPC/dir1/dir2, # dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2). if pattern.pop(): os.mkdir(path, mode) # From another process. concurrently_created.add(path) os.mkdir(path, mode) # Our real call. pattern = [bool(pattern_num & (1 << n)) for n in range(5)] concurrently_created = set() p12 = p / 'dir1' / 'dir2' try: with mock.patch("pathlib._normal_accessor.mkdir", my_mkdir): p12.mkdir(parents=True, exist_ok=False) except FileExistsError: self.assertIn(str(p12), concurrently_created) else: self.assertNotIn(str(p12), concurrently_created) self.assertTrue(p.exists()) @support.skip_unless_symlink def test_symlink_to(self): P = self.cls(BASE) target = P / 'fileA' # Symlinking a path target. link = P / 'dirA' / 'linkAA' link.symlink_to(target) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) # Symlinking a str target. link = P / 'dirA' / 'linkAAA' link.symlink_to(str(target)) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertFalse(link.is_dir()) # Symlinking to a directory. target = P / 'dirB' link = P / 'dirA' / 'linkAAAA' link.symlink_to(target, target_is_directory=True) self.assertEqual(link.stat(), target.stat()) self.assertNotEqual(link.lstat(), target.stat()) self.assertTrue(link.is_dir()) self.assertTrue(list(link.iterdir())) def test_is_dir(self): P = self.cls(BASE) self.assertTrue((P / 'dirA').is_dir()) self.assertFalse((P / 'fileA').is_dir()) self.assertFalse((P / 'non-existing').is_dir()) self.assertFalse((P / 'fileA' / 'bah').is_dir()) if support.can_symlink(): self.assertFalse((P / 'linkA').is_dir()) self.assertTrue((P / 'linkB').is_dir()) self.assertFalse((P/ 'brokenLink').is_dir(), False) self.assertIs((P / 'dirA\udfff').is_dir(), False) self.assertIs((P / 'dirA\x00').is_dir(), False) def test_is_file(self): P = self.cls(BASE) self.assertTrue((P / 'fileA').is_file()) self.assertFalse((P / 'dirA').is_file()) self.assertFalse((P / 'non-existing').is_file()) self.assertFalse((P / 'fileA' / 'bah').is_file()) if support.can_symlink(): self.assertTrue((P / 'linkA').is_file()) self.assertFalse((P / 'linkB').is_file()) self.assertFalse((P/ 'brokenLink').is_file()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) @only_posix def test_is_mount(self): P = self.cls(BASE) R = self.cls('/') # TODO: Work out Windows. self.assertFalse((P / 'fileA').is_mount()) self.assertFalse((P / 'dirA').is_mount()) self.assertFalse((P / 'non-existing').is_mount()) self.assertFalse((P / 'fileA' / 'bah').is_mount()) self.assertTrue(R.is_mount()) if support.can_symlink(): self.assertFalse((P / 'linkA').is_mount()) self.assertIs(self.cls('/\udfff').is_mount(), False) self.assertIs(self.cls('/\x00').is_mount(), False) def test_is_symlink(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_symlink()) self.assertFalse((P / 'dirA').is_symlink()) self.assertFalse((P / 'non-existing').is_symlink()) self.assertFalse((P / 'fileA' / 'bah').is_symlink()) if support.can_symlink(): self.assertTrue((P / 'linkA').is_symlink()) self.assertTrue((P / 'linkB').is_symlink()) self.assertTrue((P/ 'brokenLink').is_symlink()) self.assertIs((P / 'fileA\udfff').is_file(), False) self.assertIs((P / 'fileA\x00').is_file(), False) if support.can_symlink(): self.assertIs((P / 'linkA\udfff').is_file(), False) self.assertIs((P / 'linkA\x00').is_file(), False) def test_is_fifo_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_fifo()) self.assertFalse((P / 'dirA').is_fifo()) self.assertFalse((P / 'non-existing').is_fifo()) self.assertFalse((P / 'fileA' / 'bah').is_fifo()) self.assertIs((P / 'fileA\udfff').is_fifo(), False) self.assertIs((P / 'fileA\x00').is_fifo(), False) @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required") def test_is_fifo_true(self): P = self.cls(BASE, 'myfifo') try: os.mkfifo(str(P)) except PermissionError as e: self.skipTest('os.mkfifo(): %s' % e) self.assertTrue(P.is_fifo()) self.assertFalse(P.is_socket()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'myfifo\udfff').is_fifo(), False) self.assertIs(self.cls(BASE, 'myfifo\x00').is_fifo(), False) def test_is_socket_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_socket()) self.assertFalse((P / 'dirA').is_socket()) self.assertFalse((P / 'non-existing').is_socket()) self.assertFalse((P / 'fileA' / 'bah').is_socket()) self.assertIs((P / 'fileA\udfff').is_socket(), False) self.assertIs((P / 'fileA\x00').is_socket(), False) @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "Unix sockets required") def test_is_socket_true(self): P = self.cls(BASE, 'mysock') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.addCleanup(sock.close) try: sock.bind(str(P)) except OSError as e: if (isinstance(e, PermissionError) or "AF_UNIX path too long" in str(e)): self.skipTest("cannot bind Unix socket: " + str(e)) self.assertTrue(P.is_socket()) self.assertFalse(P.is_fifo()) self.assertFalse(P.is_file()) self.assertIs(self.cls(BASE, 'mysock\udfff').is_socket(), False) self.assertIs(self.cls(BASE, 'mysock\x00').is_socket(), False) def test_is_block_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_block_device()) self.assertFalse((P / 'dirA').is_block_device()) self.assertFalse((P / 'non-existing').is_block_device()) self.assertFalse((P / 'fileA' / 'bah').is_block_device()) self.assertIs((P / 'fileA\udfff').is_block_device(), False) self.assertIs((P / 'fileA\x00').is_block_device(), False) def test_is_char_device_false(self): P = self.cls(BASE) self.assertFalse((P / 'fileA').is_char_device()) self.assertFalse((P / 'dirA').is_char_device()) self.assertFalse((P / 'non-existing').is_char_device()) self.assertFalse((P / 'fileA' / 'bah').is_char_device()) self.assertIs((P / 'fileA\udfff').is_char_device(), False) self.assertIs((P / 'fileA\x00').is_char_device(), False) def test_is_char_device_true(self): # Under Unix, /dev/null should generally be a char device. P = self.cls('/dev/null') if not P.exists(): self.skipTest("/dev/null required") self.assertTrue(P.is_char_device()) self.assertFalse(P.is_block_device()) self.assertFalse(P.is_file()) self.assertIs(self.cls('/dev/null\udfff').is_char_device(), False) self.assertIs(self.cls('/dev/null\x00').is_char_device(), False) def test_pickling_common(self): p = self.cls(BASE, 'fileA') for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): dumped = pickle.dumps(p, proto) pp = pickle.loads(dumped) self.assertEqual(pp.stat(), p.stat()) def test_parts_interning(self): P = self.cls p = P('/usr/bin/foo') q = P('/usr/local/bin') # 'usr' self.assertIs(p.parts[1], q.parts[1]) # 'bin' self.assertIs(p.parts[2], q.parts[3]) def _check_complex_symlinks(self, link0_target): # Test solving a non-looping chain of symlinks (issue #19887). P = self.cls(BASE) self.dirlink(os.path.join('link0', 'link0'), join('link1')) self.dirlink(os.path.join('link1', 'link1'), join('link2')) self.dirlink(os.path.join('link2', 'link2'), join('link3')) self.dirlink(link0_target, join('link0')) # Resolve absolute paths. p = (P / 'link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = (P / 'link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) # Resolve relative paths. old_path = os.getcwd() os.chdir(BASE) try: p = self.cls('link0').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link1').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link2').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) p = self.cls('link3').resolve() self.assertEqual(p, P) self.assertEqualNormCase(str(p), BASE) finally: os.chdir(old_path) @support.skip_unless_symlink def test_complex_symlinks_absolute(self): self._check_complex_symlinks(BASE) @support.skip_unless_symlink def test_complex_symlinks_relative(self): self._check_complex_symlinks('.') @support.skip_unless_symlink def test_complex_symlinks_relative_dot_dot(self): self._check_complex_symlinks(os.path.join('dirA', '..')) class PathTest(_BasePathTest, unittest.TestCase): cls = UPath def test_class_getitem(self): self.assertIs(self.cls[str], self.cls) def test_concrete_class(self): p = self.cls('a') self.assertIs(type(p), WindowsUPath if os.name == 'nt' else PosixUPath) def test_unsupported_flavour(self): if os.name == 'nt': self.assertRaises(NotImplementedError, PosixUPath) else: self.assertRaises(NotImplementedError, WindowsUPath) def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): list(p.glob('')) @only_posix class PosixPathTest(_BasePathTest, unittest.TestCase): cls = PosixUPath def _check_symlink_loop(self, *args, strict=True): path = self.cls(*args) with self.assertRaises(RuntimeError): print(path.resolve(strict)) def test_open_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) with (p / 'new_file').open('wb'): pass st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) with (p / 'other_new_file').open('wb'): pass st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) def test_resolve_root(self): current_directory = os.getcwd() try: os.chdir('/') p = self.cls('spam') self.assertEqual(str(p.resolve()), '/spam') finally: os.chdir(current_directory) def test_touch_mode(self): old_mask = os.umask(0) self.addCleanup(os.umask, old_mask) p = self.cls(BASE) (p / 'new_file').touch() st = os.stat(join('new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o666) os.umask(0o022) (p / 'other_new_file').touch() st = os.stat(join('other_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o644) (p / 'masked_new_file').touch(mode=0o750) st = os.stat(join('masked_new_file')) self.assertEqual(stat.S_IMODE(st.st_mode), 0o750) @support.skip_unless_symlink def test_resolve_loop(self): # Loops with relative symlinks. os.symlink('linkX/inside', join('linkX')) self._check_symlink_loop(BASE, 'linkX') os.symlink('linkY', join('linkY')) self._check_symlink_loop(BASE, 'linkY') os.symlink('linkZ/../linkZ', join('linkZ')) self._check_symlink_loop(BASE, 'linkZ') # Non-strict self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False) # Loops with absolute symlinks. os.symlink(join('linkU/inside'), join('linkU')) self._check_symlink_loop(BASE, 'linkU') os.symlink(join('linkV'), join('linkV')) self._check_symlink_loop(BASE, 'linkV') os.symlink(join('linkW/../linkW'), join('linkW')) self._check_symlink_loop(BASE, 'linkW') # Non-strict self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False) def test_glob(self): P = self.cls p = P(BASE) given = set(p.glob("FILEa")) expect = set() if not support.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.glob("FILEa*")), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") given = set(p.rglob("FILEd")) expect = set() if not support.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) self.assertEqual(set(p.rglob("FILEd*")), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') def test_expanduser(self): P = self.cls support.import_module('pwd') import pwd pwdent = pwd.getpwuid(os.getuid()) username = pwdent.pw_name userhome = pwdent.pw_dir.rstrip('/') or '/' # Find arbitrary different user (if exists). for pwdent in pwd.getpwall(): othername = pwdent.pw_name otherhome = pwdent.pw_dir.rstrip('/') if othername != username and otherhome: break else: othername = username otherhome = userhome fakename = 'fakeuser' # This user can theoretically exist on a test runner. Create unique name: try: while pwd.getpwnam(fakename): fakename += '1' except KeyError: pass # Non-existent name found p1 = P('~/Documents') p2 = P(f'~{username}/Documents') p3 = P(f'~{othername}/Documents') p4 = P(f'../~{username}/Documents') p5 = P(f'/~{username}/Documents') p6 = P('') p7 = P(f'~{fakename}/Documents') with support.EnvironmentVarGuard() as env: env.pop('HOME', None) self.assertEqual(p1.expanduser(), P(userhome) / 'Documents') self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) env['HOME'] = '/tmp' self.assertEqual(p1.expanduser(), P('/tmp/Documents')) self.assertEqual(p2.expanduser(), P(userhome) / 'Documents') self.assertEqual(p3.expanduser(), P(otherhome) / 'Documents') self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) @unittest.skipIf(sys.platform != "darwin", "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: file_descriptors = list(UPath('/dev/fd').rglob("*"))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee # which one will fail. for f in file_descriptors: f.exists() f.is_dir() f.is_file() f.is_symlink() f.is_block_device() f.is_char_device() f.is_fifo() f.is_socket() except OSError as e: if e.errno == errno.EBADF: self.fail("Bad file descriptor not handled.") raise @only_nt class WindowsPathTest(_BasePathTest, unittest.TestCase): cls = WindowsUPath def test_glob(self): P = self.cls p = P(BASE) self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\FILEa"}) self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\FILEd"}) def test_expanduser(self): P = self.cls with support.EnvironmentVarGuard() as env: env.pop('HOME', None) env.pop('USERPROFILE', None) env.pop('HOMEPATH', None) env.pop('HOMEDRIVE', None) env['USERNAME'] = 'alice' # test that the path returns unchanged p1 = P('~/My Documents') p2 = P('~alice/My Documents') p3 = P('~bob/My Documents') p4 = P('/~/My Documents') p5 = P('d:~/My Documents') p6 = P('') self.assertRaises(RuntimeError, p1.expanduser) self.assertRaises(RuntimeError, p2.expanduser) self.assertRaises(RuntimeError, p3.expanduser) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) def check(): env.pop('USERNAME', None) self.assertEqual(p1.expanduser(), P('C:/Users/alice/My Documents')) self.assertRaises(KeyError, p2.expanduser) env['USERNAME'] = 'alice' self.assertEqual(p2.expanduser(), P('C:/Users/alice/My Documents')) self.assertEqual(p3.expanduser(), P('C:/Users/bob/My Documents')) self.assertEqual(p4.expanduser(), p4) self.assertEqual(p5.expanduser(), p5) self.assertEqual(p6.expanduser(), p6) env['HOMEPATH'] = 'C:\\Users\\alice' check() env['HOMEDRIVE'] = 'C:\\' env['HOMEPATH'] = 'Users\\alice' check() env.pop('HOMEDRIVE', None) env.pop('HOMEPATH', None) env['USERPROFILE'] = 'C:\\Users\\alice' check() # bpo-38883: ignore `HOME` when set on windows env['HOME'] = 'C:\\Users\\eve' check() universal_pathlib-0.3.10/upath/tests/test_chain.py000066400000000000000000000063411514661127100222740ustar00rootroot00000000000000import os from pathlib import Path import pytest from fsspec.implementations.memory import MemoryFileSystem from upath import UPath from upath._chain import FSSpecChainParser @pytest.mark.parametrize( "urlpath,expected", [ ("simplecache::file://tmp", "simplecache"), ("zip://file.txt::file://tmp.zip", "zip"), ], ) def test_chaining_upath_protocol(urlpath, expected): pth = UPath(urlpath) assert pth.protocol == expected def add_current_drive_on_windows(pth: str) -> str: drive = os.path.splitdrive(Path.cwd().as_posix())[0] return f"{drive}{pth}" @pytest.mark.parametrize( "urlpath,expected", [ pytest.param( "simplecache::file:///tmp", add_current_drive_on_windows("/tmp"), ), pytest.param( "zip://file.txt::file:///tmp.zip", "file.txt", ), pytest.param( "zip://a/b/c.txt::simplecache::memory://zipfile.zip", "a/b/c.txt", ), ], ) def test_chaining_upath_path(urlpath, expected): pth = UPath(urlpath) assert pth.path == expected @pytest.mark.parametrize( "urlpath,expected", [ ( "simplecache::file:///tmp", { "target_protocol": "file", "target_options": {}, }, ), ], ) def test_chaining_upath_storage_options(urlpath, expected): pth = UPath(urlpath) assert dict(pth.storage_options) == expected @pytest.mark.parametrize( "urlpath,expected", [ ("simplecache::memory://tmp", ("/", "tmp")), ], ) def test_chaining_upath_parts(urlpath, expected): pth = UPath(urlpath) assert pth.parts == expected @pytest.mark.parametrize( "urlpath,expected", [ ("simplecache::memory:///tmp", "simplecache::memory:///tmp"), ], ) def test_chaining_upath_str(urlpath, expected): pth = UPath(urlpath) assert str(pth) == expected @pytest.fixture def clear_memory_fs(): fs = MemoryFileSystem() store = fs.store pseudo_dirs = fs.pseudo_dirs try: yield fs finally: fs.store.clear() fs.store.update(store) fs.pseudo_dirs[:] = pseudo_dirs @pytest.fixture def memory_file_urlpath(clear_memory_fs): fs = clear_memory_fs fs.pipe_file("/abc/file.txt", b"hello world") yield fs.unstrip_protocol("/abc/file.txt") def test_read_file(memory_file_urlpath): pth = UPath(f"simplecache::{memory_file_urlpath}") assert pth.read_bytes() == b"hello world" def test_write_file(clear_memory_fs): pth = UPath("simplecache::memory://abc.txt") pth.write_bytes(b"hello world") assert clear_memory_fs.cat_file("abc.txt") == b"hello world" @pytest.mark.parametrize( "urlpath", [ "memory:///file.txt", "simplecache::memory:///tmp", "zip://file.txt::memory:///tmp.zip", "zip://a/b/c.txt::simplecache::memory:///zipfile.zip", "simplecache::zip://a/b/c.txt::tar://blah.zip::memory:///file.tar", ], ) def test_chain_parser_roundtrip(urlpath: str): parser = FSSpecChainParser() segments = parser.unchain(urlpath, protocol=None, storage_options={}) rechained, kw = parser.chain(segments) assert rechained == urlpath assert kw == {} universal_pathlib-0.3.10/upath/tests/test_core.py000066400000000000000000000423451514661127100221460ustar00rootroot00000000000000import os import pathlib import pickle import sys import warnings from urllib.parse import SplitResult import pathlib_abc import pytest from upath import UPath from upath.implementations.cloud import GCSPath from upath.implementations.cloud import S3Path from upath.registry import get_upath_class from upath.registry import register_implementation from upath.types import ReadablePath from upath.types import WritablePath from .cases import BaseTests from .utils import OverrideMeta from .utils import only_on_windows from .utils import overrides_base from .utils import skip_on_windows @skip_on_windows def test_posix_path(local_testdir): assert isinstance(UPath(local_testdir), pathlib.PosixPath) @only_on_windows def test_windows_path(local_testdir): assert isinstance(UPath(local_testdir), pathlib.WindowsPath) def test_UPath_untested_protocol_warning(clear_registry): with warnings.catch_warnings(record=True) as w: _ = UPath("mock:/") assert len(w) == 1 assert issubclass(w[-1].category, UserWarning) assert "mock" in str(w[-1].message) def test_UPath_file_protocol_no_warning(): with warnings.catch_warnings(record=True) as w: _ = UPath("file:/") assert len(w) == 0 class TestUpath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): with warnings.catch_warnings(): warnings.simplefilter("ignore") # On Windows the path needs to be prefixed with `/`, because # `UPath` implements `_posix_flavour`, which requires a `/` root # in order to correctly deserialize pickled objects root = "/" if sys.platform.startswith("win") else "" self.path = UPath(f"mock:{root}{local_testdir}") @overrides_base def test_is_correct_class(self): # testing dynamically created UPath classes from upath.implementations._experimental import _MockPath assert isinstance(self.path, _MockPath) @overrides_base @pytest.mark.skipif( sys.platform.startswith("win"), reason="mock fs is not well defined on windows", ) def test_parents_are_absolute(self): super().test_parents_are_absolute() @overrides_base @pytest.mark.skipif( sys.platform.startswith("win"), reason="mock fs is not well defined on windows", ) def test_anchor_is_its_own_parent(self): super().test_anchor_is_its_own_parent() @overrides_base @pytest.mark.skipif( sys.platform.startswith("win"), reason="mock fs is not well defined on windows", ) def test_parents_end_at_anchor(self): super().test_parents_end_at_anchor() def test_multiple_backend_paths(local_testdir): path = "s3://bucket/" s3_path = UPath(path, anon=True) assert s3_path.joinpath("text.txt")._url.scheme == "s3" path = f"file://{local_testdir}" UPath(path) assert s3_path.joinpath("text1.txt")._url.scheme == "s3" def test_constructor_accept_path(local_testdir): path = UPath(pathlib.Path(local_testdir)) assert str(path) == str(pathlib.Path(local_testdir)) def test_constructor_accept_upath(local_testdir): path = UPath(UPath(local_testdir)) assert str(path) == str(pathlib.Path(local_testdir)) def test_subclass(local_testdir): class MyPath(UPath): pass with pytest.raises(ValueError, match=r".*incompatible with"): MyPath(local_testdir) @pytest.fixture(scope="function") def upath_registry_snapshot(): """Save and restore the upath registry state around a test.""" from upath.registry import _registry # Save the current state of the registry's mutable mapping saved_m = _registry._m.maps[0].copy() try: yield finally: # Restore the registry state _registry._m.maps[0].clear() _registry._m.maps[0].update(saved_m) get_upath_class.cache_clear() def test_subclass_registered(upath_registry_snapshot): class MyPath(UPath): pass register_implementation("memory", MyPath, clobber=True) path = MyPath("memory:///test_path") assert str(path) == "memory:///test_path" assert issubclass(MyPath, UPath) assert isinstance(path, MyPath) assert isinstance(path, pathlib_abc.ReadablePath) assert isinstance(path, pathlib_abc.WritablePath) assert not isinstance(path, pathlib.Path) def test_subclass_with_gcs(): path = UPath("gcs://bucket", anon=True) assert isinstance(path, UPath) assert isinstance(path, ReadablePath) assert isinstance(path, WritablePath) assert not isinstance(path, os.PathLike) assert not isinstance(path, pathlib.Path) def test_instance_check(local_testdir): path = UPath(local_testdir) # test instance check passes assert isinstance(path, UPath) assert isinstance(path, ReadablePath) assert isinstance(path, WritablePath) assert isinstance(path, os.PathLike) assert isinstance(path, pathlib.Path) def test_instance_check_local_uri(local_testdir): path = UPath(f"file://{local_testdir}") assert isinstance(path, UPath) assert isinstance(path, ReadablePath) assert isinstance(path, WritablePath) assert isinstance(path, os.PathLike) assert not isinstance(path, pathlib.Path) @pytest.mark.xfail(reason="unsupported on universal_pathlib>0.1.4") def test_new_method(local_testdir): path = UPath.__new__(pathlib.Path, local_testdir) assert str(path) == str(pathlib.Path(local_testdir)) assert isinstance(path, pathlib.Path) assert isinstance(path, UPath) PATHS = ( ("path", "storage_options", "module", "object_type"), ( ("/tmp/abc", {}, None, pathlib.Path), ("s3://bucket/folder", {"anon": True}, "s3fs", S3Path), ("gs://bucket/folder", {"token": "anon"}, "gcsfs", GCSPath), ), ) @pytest.mark.parametrize(*PATHS) def test_create_from_type(path, storage_options, module, object_type): """Test that derived paths use same fs instance.""" if module: # skip if module cannot be imported pytest.importorskip(module) upath = UPath(path, **storage_options) # test expected object type assert isinstance(upath, object_type) cast = type(upath) parent = upath.parent # test derived object is same type assert isinstance(parent, cast) # test that created fs uses fsspec instance cache assert upath.fs is parent.fs new = cast(str(parent), **storage_options) # test that object cast is same type assert isinstance(new, cast) def test_list_args(): path_a = UPath("gcs://bucket", "folder") path_b = UPath("gcs://bucket") / "folder" assert str(path_a) == str(path_b) assert path_a.root == path_b.root assert path_a.drive == path_b.drive assert path_a.parts == path_b.parts assert path_a._url == path_b._url def test_child_path(): path_a = UPath("gcs://bucket/folder") path_b = UPath("gcs://bucket") / "folder" assert str(path_a) == str(path_b) assert path_a.root == path_b.root assert path_a.drive == path_b.drive assert path_a.parts == path_b.parts assert path_a._url == path_b._url def test_pickling(): path = UPath("gcs://bucket/folder", token="anon") pickled_path = pickle.dumps(path) recovered_path = pickle.loads(pickled_path) assert type(path) is type(recovered_path) assert str(path) == str(recovered_path) assert path.storage_options == recovered_path.storage_options def test_pickling_child_path(): path = UPath("gcs://bucket", token="anon") / "subfolder" / "subsubfolder" pickled_path = pickle.dumps(path) recovered_path = pickle.loads(pickled_path) assert type(path) is type(recovered_path) assert str(path) == str(recovered_path) assert path.drive == recovered_path.drive assert path.root == recovered_path.root assert path.parts == recovered_path.parts assert path.storage_options == recovered_path.storage_options def test_copy_path(): path = UPath("gcs://bucket/folder", token="anon") copy_path = UPath(path) assert type(path) is type(copy_path) assert str(path) == str(copy_path) assert path.drive == copy_path.drive assert path.root == copy_path.root assert path.parts == copy_path.parts assert path.storage_options == copy_path.storage_options def test_copy_path_posix(): path = UPath("/tmp/folder") copy_path = UPath(path) assert type(path) is type(copy_path) assert str(path) == str(copy_path) assert path.drive == copy_path.drive assert path.root == copy_path.root assert path.parts == copy_path.parts def test_copy_path_append(): path = UPath("/tmp/folder") copy_path = UPath(path, "folder2") assert type(path) is type(copy_path) assert str(path / "folder2") == str(copy_path) path = UPath("/tmp/folder") copy_path = UPath(path, "folder2/folder3") assert str(path / "folder2" / "folder3") == str(copy_path) path = UPath("/tmp/folder") copy_path = UPath(path, "folder2", "folder3") assert str(path / "folder2" / "folder3") == str(copy_path) def test_compare_to_pathlib_path_ne(): assert UPath("gcs://bucket/folder") != pathlib.Path("gcs://bucket/folder") assert pathlib.Path("gcs://bucket/folder") != UPath("gcs://bucket/folder") assert UPath("/bucket/folder") == pathlib.Path("/bucket/folder") assert pathlib.Path("/bucket/folder") == UPath("/bucket/folder") def test_handle_fspath_args(tmp_path): f = tmp_path.joinpath("file.txt").as_posix() class X: def __str__(self): raise ValueError("should not be called") def __fspath__(self): return f assert UPath(X()).path == f @pytest.mark.parametrize( "urlpath", [ os.getcwd(), pathlib.Path.cwd().as_uri(), pytest.param( "mock:///abc", marks=pytest.mark.skipif( os.name == "nt", reason="_url not well defined for mock filesystem on windows", ), ), ], ) def test_access_to_private_kwargs_and_url(urlpath): p0 = UPath(urlpath) assert not hasattr(p0, "_kwargs") # fixme: this should be deprecated... assert isinstance(p0._url, SplitResult) assert p0._url.scheme == "" or p0._url.scheme in p0.fs.protocol assert p0._url.path == p0.path p1 = p0 / "foo" assert isinstance(p1._url, SplitResult) assert p1._url.scheme == "" or p1._url.scheme in p1.fs.protocol assert p1._url.path == p1.path def test_copy_path_append_kwargs(): path = UPath("gcs://bucket/folder", anon=True) copy_path = UPath(path, anon=False) assert type(path) is type(copy_path) assert str(path) == str(copy_path) assert not copy_path.storage_options["anon"] assert path.storage_options["anon"] def test_relative_to(): assert "file.txt" == str( UPath("s3://test_bucket/file.txt").relative_to(UPath("s3://test_bucket")) ) with pytest.raises(ValueError): UPath("s3://test_bucket/file.txt").relative_to(UPath("gcs://test_bucket")) with pytest.raises(ValueError): UPath("s3://test_bucket/file.txt", anon=True).relative_to( UPath("s3://test_bucket", anon=False) ) def test_uri_parsing(): assert ( str(UPath("http://www.example.com//a//b/")) == "http://www.example.com//a//b/" ) NORMALIZATIONS = ( ("unnormalized", "normalized"), ( # Normalization with and without an authority component ("memory:/a/b/..", "memory://a/"), ("memory:/a/b/.", "memory://a/b/"), ("memory:/a/b/../..", "memory://"), ("memory:/a/b/../../..", "memory://"), ("memory://a/b/.", "memory://a/b/"), ("memory://a/b/..", "memory://a/"), ("memory://a/b/../..", "memory://"), ("memory://a/b/../../..", "memory://"), ("memory:///a/b/.", "memory://a/b/"), ("memory:///a/b/..", "memory://a/"), ("memory:///a/b/../..", "memory://"), ("memory:///a/b/../../..", "memory://"), ), ) @pytest.mark.parametrize(*NORMALIZATIONS) def test_normalize(unnormalized, normalized): expected = UPath(normalized) pth = UPath(unnormalized) result = pth.resolve(strict=True) str_expected = str(expected) str_result = str(result) assert expected == result assert str_expected == str_result @pytest.mark.parametrize( "uri,query_str", [ ("s3://bucket/folder?versionId=1", "?versionId=1"), ("http://example.com/abc?p=2", "?p=2"), ], ) def test_query_string(uri, query_str): p = UPath(uri) assert str(p).endswith(query_str) assert p.path.endswith(query_str) PROTOCOL_MISMATCH = [ ("/a", "s3://bucket/b"), ("s3://bucket/a", "gs://b/c"), ("gs://bucket/a", "memory://b/c"), ("memory://bucket/a", "s3://b/c"), ] @pytest.mark.parametrize("base,join", PROTOCOL_MISMATCH) def test_joinpath_on_protocol_mismatch(base, join): with pytest.raises(ValueError, match="can't combine incompatible UPath protocols"): UPath(base).joinpath(UPath(join)) @pytest.mark.parametrize("base,join", PROTOCOL_MISMATCH) def test_truediv_on_protocol_mismatch(base, join): with pytest.raises(ValueError, match="can't combine incompatible UPath protocols"): UPath(base) / UPath(join) @pytest.mark.parametrize("base,join", PROTOCOL_MISMATCH) def test_joinuri_on_protocol_mismatch(base, join): assert UPath(base).joinuri(UPath(join)) == UPath(join) def test_upath_expanduser(): assert UPath("~").expanduser() == UPath(os.path.expanduser("~")) assert UPath("~") != UPath("~").expanduser() def test_builtin_open_a_non_local_upath(): p = UPath("memory://a") p.write_bytes(b"hello world") with pytest.raises(TypeError, match="expected str, bytes or os.PathLike object.*"): open(p, "rb") # type: ignore @pytest.mark.parametrize( "protocol", [ pytest.param(None, id="empty protocol"), pytest.param("file", id="file protocol"), ], ) def test_open_a_local_upath(tmp_path, protocol): p = tmp_path.joinpath("file.txt") p.write_bytes(b"hello world") u = UPath(p, protocol=protocol) with open(u, "rb") as f: assert f.read() == b"hello world" @pytest.mark.parametrize( "uri,protocol", [ # s3 compatible protocols ("s3://bucket/folder", "s3"), ("s3a://bucket/folder", "s3a"), ("bucket/folder", "s3"), # gcs compatible ("gs://bucket/folder", "gs"), ("gcs://bucket/folder", "gcs"), ("bucket/folder", "gs"), # azure compatible ("az://container/blob", "az"), ("abfs://container/blob", "abfs"), ("abfss://container/blob", "abfss"), ("adl://container/blob", "adl"), # memory ("memory://folder", "memory"), ("/folder", "memory"), # file/local ("file:/tmp/folder", "file"), ("/tmp/folder", "file"), ("file:/tmp/folder", "local"), ("/tmp/folder", "local"), ("/tmp/folder", ""), ("a/b/c", ""), # http/https ("http://example.com/path", "http"), ("https://example.com/path", "https"), # ftp ("ftp://example.com/path", "ftp"), # sftp/ssh ("sftp://example.com/path", "sftp"), ("ssh://example.com/path", "ssh"), # smb ("smb://server/share/path", "smb"), # hdfs ("hdfs://namenode/path", "hdfs"), # webdav - requires base_url, skip for now # github ("github://owner:repo@branch/path", "github"), # data ("data:text/plain;base64,SGVsbG8=", "data"), # huggingface ("hf://datasets/user/repo/path", "hf"), ], ) def test_constructor_compatible_protocol_uri(uri, protocol): p = UPath(uri, protocol=protocol) assert p.protocol == protocol # Protocol to sample URI mapping _PROTOCOL_URIS = { "s3": "s3://bucket/folder", "gs": "gs://bucket/folder", "az": "az://container/blob", "memory": "memory://folder", "file": "file:/tmp/folder", "http": "http://example.com/path", "ftp": "ftp://example.com/path", "sftp": "sftp://example.com/path", "smb": "smb://server/share/path", "hdfs": "hdfs://namenode/path", } # Generate incompatible combinations: each protocol with URIs from other protocols _INCOMPATIBLE_CASES = [ (_PROTOCOL_URIS[uri_protocol], target_protocol) for target_protocol in _PROTOCOL_URIS for uri_protocol in _PROTOCOL_URIS if target_protocol != uri_protocol ] # Also test explicit empty protocol with protocol-prefixed URIs _INCOMPATIBLE_CASES.extend([(uri, "") for uri in _PROTOCOL_URIS.values()]) @pytest.mark.parametrize("uri,protocol", _INCOMPATIBLE_CASES) def test_constructor_incompatible_protocol_uri(uri, protocol): with pytest.raises(TypeError, match=r".*incompatible with"): UPath(uri, protocol=protocol) # Test subclass instantiation with incompatible URIs # Use protocols that have registered implementations we can get via get_upath_class _SUBCLASS_INCOMPATIBLE_CASES = [ (_PROTOCOL_URIS[uri_protocol], target_protocol) for target_protocol in _PROTOCOL_URIS for uri_protocol in _PROTOCOL_URIS if target_protocol != uri_protocol ] @pytest.mark.parametrize("uri,protocol", _SUBCLASS_INCOMPATIBLE_CASES) def test_subclass_constructor_incompatible_protocol_uri(uri, protocol): cls = get_upath_class(protocol) with pytest.raises(TypeError, match=r".*incompatible with"): cls(uri) universal_pathlib-0.3.10/upath/tests/test_drive_root_anchor_parts.py000066400000000000000000000043771514661127100261400ustar00rootroot00000000000000from pathlib import Path import pytest from upath import UPath DRIVE_ROOT_ANCHOR_TESTS = [ # cloud ("s3://bucket", "bucket", "/", "bucket/", ("bucket/",)), ("s3://bucket/a", "bucket", "/", "bucket/", ("bucket/", "a")), ("gs://bucket", "bucket", "/", "bucket/", ("bucket/",)), ("gs://bucket/a", "bucket", "/", "bucket/", ("bucket/", "a")), ("az://bucket", "bucket", "/", "bucket/", ("bucket/",)), ("az://bucket/a", "bucket", "/", "bucket/", ("bucket/", "a")), # data ( "data:text/plain,A%20brief%20note", "", "", "", ("data:text/plain,A%20brief%20note",), ), # github ("github://user:token@repo/abc", "", "", "", ("abc",)), # hdfs ("hdfs://a/b/c", "", "/", "/", ("/", "b", "c")), ("hdfs:///a/b/c", "", "/", "/", ("/", "a", "b", "c")), # http ("http://a/", "http://a", "/", "http://a/", ("http://a/", "")), ("http://a/b/c", "http://a", "/", "http://a/", ("http://a/", "b", "c")), ("https://a/b/c", "https://a", "/", "https://a/", ("https://a/", "b", "c")), # memory ("memory://a/b/c", "", "/", "/", ("/", "a", "b", "c")), ("memory:///a/b/c", "", "/", "/", ("/", "a", "b", "c")), # sftp ("sftp://a/b/c", "", "/", "/", ("/", "b", "c")), ("sftp:///a/b/c", "", "/", "/", ("/", "a", "b", "c")), # smb ("smb://a/b/c", "", "/", "/", ("/", "b", "c")), ("smb:///a/b/c", "", "/", "/", ("/", "a", "b", "c")), # webdav ("webdav+http://host.com/a/b/c", "", "", "", ("a", "b", "c")), ("webdav+http://host.com/a/b/c", "", "", "", ("a", "b", "c")), # local ( "file:///a/b/c", Path("/a/b/c").absolute().drive, Path("/").absolute().root.replace("\\", "/"), Path("/").absolute().anchor.replace("\\", "/"), tuple(x.replace("\\", "/") for x in Path("/a/b/c").absolute().parts), ), ] @pytest.mark.parametrize( "path,drive,root,anchor", [x[0:4] for x in DRIVE_ROOT_ANCHOR_TESTS], ) def test_drive_root_anchor(path, drive, root, anchor): p = UPath(path) assert (p.drive, p.root, p.anchor) == (drive, root, anchor) @pytest.mark.parametrize( "path,parts", [(x[0], x[4]) for x in DRIVE_ROOT_ANCHOR_TESTS], ) def test_parts(path, parts): p = UPath(path) assert p.parts == parts universal_pathlib-0.3.10/upath/tests/test_extensions.py000066400000000000000000000170411514661127100234100ustar00rootroot00000000000000import os import sys from contextlib import nullcontext import pytest from upath import UnsupportedOperation from upath import UPath from upath.extensions import ProxyUPath from upath.implementations.local import FilePath from upath.implementations.local import PosixUPath from upath.implementations.local import WindowsUPath from upath.implementations.memory import MemoryPath from .cases import BaseTests from .utils import OverrideMeta from .utils import extends_base from .utils import overrides_base class TestProxyMemoryPath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): if not local_testdir.startswith("/"): local_testdir = "/" + local_testdir self.path = ProxyUPath(f"memory:{local_testdir}") self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, ProxyUPath) @extends_base def test_is_not_wrapped_class(self): assert not isinstance(self.path, MemoryPath) class TestProxyFilePath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): self.path = ProxyUPath(f"file://{local_testdir}") self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, ProxyUPath) @extends_base def test_is_not_wrapped_class(self): assert not isinstance(self.path, FilePath) @overrides_base def test_chmod(self): self.path.joinpath("file1.txt").chmod(777) @overrides_base def test_cwd(self): # ProxyUPath.cwd() works differently on the instance self.path.cwd() with pytest.raises(UnsupportedOperation): type(self.path).cwd() class TestProxyPathlibPath(BaseTests, metaclass=OverrideMeta): @pytest.fixture(autouse=True) def path(self, local_testdir): self.path = ProxyUPath(f"{local_testdir}") self.prepare_file_system() @overrides_base def test_is_correct_class(self): assert isinstance(self.path, ProxyUPath) @extends_base def test_is_not_wrapped_class(self): assert not isinstance(self.path, (PosixUPath, WindowsUPath)) @overrides_base def test_chmod(self): self.path.joinpath("file1.txt").chmod(777) @overrides_base @pytest.mark.skipif( sys.version_info < (3, 12), reason="storage options only handled in 3.12+" ) def test_eq(self): super().test_eq() if sys.version_info < (3, 12): @overrides_base def test_storage_options_dont_affect_hash(self): # On Python < 3.12, storage_options trigger warnings for LocalPath with pytest.warns( UserWarning, match=r".*on python <= \(3, 11\) ignores protocol and storage_options", ): super().test_storage_options_dont_affect_hash() @overrides_base def test_group(self): pytest.importorskip("grp") self.path.group() @overrides_base def test_owner(self): pytest.importorskip("pwd") self.path.owner() @overrides_base def test_readlink(self): try: os.readlink except AttributeError: pytest.skip("os.readlink not available on this platform") with pytest.raises((OSError, UnsupportedOperation)): self.path.readlink() @overrides_base def test_protocol(self): assert self.path.protocol == "" @overrides_base def test_as_uri(self): assert self.path.as_uri().startswith("file://") if sys.version_info < (3, 10): @overrides_base def test_lstat(self): # On Python < 3.10, stat(follow_symlinks=False) triggers warnings with pytest.warns( UserWarning, match=r".*stat\(\) follow_symlinks=False is currently ignored", ): st = self.path.lstat() assert st is not None else: @overrides_base def test_lstat(self): st = self.path.lstat() assert st is not None @overrides_base def test_relative_to(self): base = self.path child = self.path / "folder1" / "file1.txt" relative = child.relative_to(base) assert str(relative) == f"folder1{os.sep}file1.txt" @overrides_base def test_cwd(self): self.path.cwd() with pytest.raises(UnsupportedOperation): type(self.path).cwd() @overrides_base def test_lchmod(self): # setup a = self.path.joinpath("a") b = self.path.joinpath("b") a.touch() b.symlink_to(a) # see: https://github.com/python/cpython/issues/108660#issuecomment-1854645898 if hasattr(os, "lchmod") or os.chmod in os.supports_follow_symlinks: cm = nullcontext() else: cm = pytest.raises((UnsupportedOperation, NotImplementedError)) with cm: b.lchmod(mode=0o777) @overrides_base def test_symlink_to(self): self.path.joinpath("link").symlink_to(self.path) @overrides_base def test_hardlink_to(self): try: self.path.joinpath("link").hardlink_to(self.path) except PermissionError: pass # hardlink may require elevated permissions def test_custom_subclass(): class ReversePath(ProxyUPath): def read_bytes_reversed(self): return self.read_bytes()[::-1] def write_bytes_reversed(self, value): self.write_bytes(value[::-1]) b = MemoryPath("memory://base") p = b.joinpath("file1") p.write_bytes(b"dlrow olleh") r = ReversePath("memory://base/file1") assert r.read_bytes_reversed() == b"hello world" r.parent.joinpath("file2").write_bytes_reversed(b"dlrow olleh") assert b.joinpath("file2").read_bytes() == b"hello world" def test_protocol_dispatch_deprecation_warning(): class MyPath(UPath): _protocol_dispatch = False with pytest.warns(DeprecationWarning, match="_protocol_dispatch = False"): a = MyPath(".", protocol="memory") assert isinstance(a, MyPath) # Protocol to sample URI mapping for compatibility tests _PROTOCOL_URIS = { "s3": "s3://bucket/folder", "gs": "gs://bucket/folder", "memory": "memory://folder", "file": "file:/tmp/folder", "http": "http://example.com/path", "": "/tmp/folder", } # Generate incompatible combinations _PROXY_INCOMPATIBLE_CASES = [ (_PROTOCOL_URIS[uri_protocol], target_protocol) for target_protocol in _PROTOCOL_URIS for uri_protocol in _PROTOCOL_URIS if target_protocol != uri_protocol and uri_protocol != "" ] @pytest.mark.parametrize("uri,protocol", _PROXY_INCOMPATIBLE_CASES) def test_proxy_subclass_incompatible_protocol_uri(uri, protocol): """Test that ProxyUPath subclasses raise TypeError for incompatible protocols.""" class MyProxyPath(ProxyUPath): pass # ProxyUPath wraps the underlying path, so it should also raise TypeError with pytest.raises(TypeError, match=r".*incompatible with"): MyProxyPath(uri, protocol=protocol) def test_proxy_upath_copy_from_local(tmp_path): """Test ProxyUPath accepts extra kwargs in _copy_from() Regression test for https://github.com/fsspec/universal_pathlib/issues/546 """ class MyProxyPath(ProxyUPath): pass source = UPath(tmp_path / "test.txt") source.write_text("hello") destination = MyProxyPath("memory://bla/test_copy_from.txt") source.move(destination) assert destination.read_text() == "hello" universal_pathlib-0.3.10/upath/tests/test_pathlib_backport.py000066400000000000000000000207021514661127100245170ustar00rootroot00000000000000import os import sys from contextlib import nullcontext from pathlib import PosixPath from pathlib import WindowsPath import pytest from upath import UnsupportedOperation from upath import UPath from upath.implementations.local import PosixUPath from upath.implementations.local import WindowsUPath def test_provides_unsupportedoperation(): assert issubclass(UnsupportedOperation, NotImplementedError) _win_only = pytest.mark.skipif(os.name != "nt", reason="windows only") _posix_only = pytest.mark.skipif(os.name == "nt", reason="posix only") @pytest.mark.parametrize( "wrong_cls", [ pytest.param(PosixPath, marks=_win_only), pytest.param(PosixUPath, marks=_win_only), pytest.param(WindowsPath, marks=_posix_only), pytest.param(WindowsUPath, marks=_posix_only), ], ) def test_unsupportedoperation_catches_pathlib_errors(wrong_cls): with pytest.raises(UnsupportedOperation): wrong_cls(".") @pytest.fixture(params=["", "file", "memory"]) def pth(request, tmp_path, clear_fsspec_memory_cache): pth = UPath(tmp_path, protocol=request.param) yield pth @pytest.fixture def pth_file(pth): f = pth.joinpath("testfile") f.touch() return f @pytest.mark.parametrize( "pth", [ pytest.param( "", marks=pytest.mark.xfail( sys.version_info < (3, 10), reason="parents getitem differs" ), ), "file", "memory", ], indirect=True, ) def test_signature_parents_getitem(pth): pth.parents[-1] pth.parents[0:2] def test_signature_stat_follow_symlinks(pth_file): pth_file.stat(follow_symlinks=True) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*write_text\(\) newline is currently ignored", ) if sys.version_info < (3, 10) else nullcontext() ), ), ("file", nullcontext()), ("memory", nullcontext()), ], indirect=["pth"], ) def test_signature_write_text_newline(pth, ctx): with ctx: _ = pth.joinpath("somename").write_text("test", newline="\n") @pytest.mark.parametrize("pth", ["", "file"], indirect=True) def test_signature_chmod_follow_symlinks(pth): _ = pth.chmod(0o777, follow_symlinks=True) def test_signature_match_pathlike(pth): _ = pth.match(UPath("*.py")) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*match\(\): case_sensitive is currently ignored", ) if sys.version_info < (3, 12) else nullcontext() ), ), ( "file", pytest.warns( UserWarning, match=r".*match\(\): case_sensitive is currently ignored" ), ), ( "memory", pytest.warns( UserWarning, match=r".*match\(\): case_sensitive is currently ignored" ), ), ], indirect=["pth"], ) def test_signature_match_case_sensitive(pth, ctx): with ctx: _ = pth.match("*.py", case_sensitive=True) def test_signature_exists_follow_symlinks(pth): _ = pth.exists(follow_symlinks=True) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*glob\(\): case_sensitive is currently ignored", ) if sys.version_info < (3, 12) else nullcontext() ), ), ( "file", pytest.warns( UserWarning, match=r".*glob\(\): case_sensitive is currently ignored" ), ), ( "memory", pytest.warns( UserWarning, match=r".*glob\(\): case_sensitive is currently ignored" ), ), ], indirect=["pth"], ) def test_signature_glob_case_sensitive(pth, ctx): with ctx: _ = list(pth.parent.glob("*", case_sensitive=True)) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*(glob|rglob)\(\): case_sensitive is currently ignored", ) if sys.version_info < (3, 12) else nullcontext() ), ), ( "file", pytest.warns( UserWarning, match=r".*(glob|rglob)\(\): case_sensitive is currently ignored", ), ), ( "memory", pytest.warns( UserWarning, match=r".*(glob|rglob)\(\): case_sensitive is currently ignored", ), ), ], indirect=["pth"], ) def test_signature_rglob_case_sensitive(pth, ctx): with ctx: _ = list(pth.parent.rglob("*", case_sensitive=True)) def test_signature_relative_to_walk_up(pth): _ = pth.relative_to(pth.parent.parent, walk_up=False) def test_signature_is_file_follow_symlinks(pth): _ = pth.is_file(follow_symlinks=True) def test_signature_is_dir_follow_symlinks(pth): _ = pth.is_dir(follow_symlinks=True) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*read_text\(\): newline is currently ignored", ) if sys.version_info < (3, 13) else nullcontext() ), ), ("file", nullcontext()), ("memory", nullcontext()), ], indirect=["pth"], ) def test_signature_read_text_newline(pth, ctx): p0 = pth.joinpath("test") p0.write_text("test") with ctx: _ = p0.read_text(newline="\n") @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=r".*glob\(\): recurse_symlinks=True is currently ignored", ) if sys.version_info < (3, 13) else nullcontext() ), ), ( "file", pytest.warns( UserWarning, match=r".*glob\(\): recurse_symlinks=True is currently ignored", ), ), ( "memory", pytest.warns( UserWarning, match=r".*glob\(\): recurse_symlinks=True is currently ignored", ), ), ], indirect=["pth"], ) def test_signature_glob_recurse_symlinks(pth, ctx): with ctx: _ = list(pth.parent.glob("**/*.py", recurse_symlinks=True)) def test_signature_glob_pathlike(pth): _ = list(pth.parent.glob(UPath("**/*.py"))) @pytest.mark.parametrize( "pth,ctx", [ ( "", ( pytest.warns( UserWarning, match=( r".*(glob|rglob)\(\):" r" recurse_symlinks=True is currently ignored" ), ) if sys.version_info < (3, 13) else nullcontext() ), ), ( "file", pytest.warns( UserWarning, match=r".*(glob|rglob)\(\): recurse_symlinks=True is currently ignored", ), ), ( "memory", pytest.warns( UserWarning, match=r".*(glob|rglob)\(\): recurse_symlinks=True is currently ignored", ), ), ], indirect=["pth"], ) def test_signature_rglob_recurse_symlinks(pth, ctx): with ctx: _ = list(pth.parent.rglob("*.py", recurse_symlinks=True)) def test_signature_rglob_pathlike(pth): _ = list(pth.parent.rglob(UPath("*.py"))) @pytest.mark.parametrize("pth", [""], indirect=True) @pytest.mark.skipif(os.name == "nt", reason="owner/group not available on windows") def test_signature_owner_follow_symlinks(pth): _ = pth.owner(follow_symlinks=True) @pytest.mark.parametrize("pth", [""], indirect=True) @pytest.mark.skipif(os.name == "nt", reason="owner/group not available on windows") def test_signature_group_follow_symlinks(pth): _ = pth.group(follow_symlinks=True) universal_pathlib-0.3.10/upath/tests/test_pydantic.py000066400000000000000000000101551514661127100230230ustar00rootroot00000000000000import json from os.path import abspath import pydantic import pydantic_core import pytest from fsspec.implementations.http import get_client from upath import UPath from upath.implementations.local import FilePath from upath.implementations.local import PosixUPath from upath.implementations.local import WindowsUPath from .utils import only_on_windows from .utils import skip_on_windows @pytest.mark.parametrize( "path", [ "/abc", "file:///abc", "memory://abc", "s3://bucket/key", "https://www.example.com", ], ) @pytest.mark.parametrize("source", ["json", "python"]) def test_validate_from_str(path, source): expected = UPath(path) ta = pydantic.TypeAdapter(UPath) if source == "json": actual = ta.validate_json(json.dumps(path)) else: # source == "python" actual = ta.validate_python(path) assert abspath(actual.path) == abspath(expected.path) assert actual.protocol == expected.protocol @pytest.mark.parametrize( "dct", [ { "path": "/my/path", "protocol": "file", "storage_options": {"foo": "bar", "baz": 3}, } ], ) @pytest.mark.parametrize("source", ["json", "python"]) def test_validate_from_dict(dct, source): ta = pydantic.TypeAdapter(UPath) if source == "json": output = ta.validate_json(json.dumps(dct)) else: # source == "python" output = ta.validate_python(dct) assert abspath(output.path) == abspath(dct["path"]) assert output.protocol == dct["protocol"] assert output.storage_options == dct["storage_options"] @pytest.mark.parametrize( "path", [ "/abc", "file:///abc", "memory://abc", "s3://bucket/key", "https://www.example.com", ], ) def test_validate_from_instance(path): input = UPath(path) output = pydantic.TypeAdapter(UPath).validate_python(input) assert output is input @pytest.mark.parametrize( ("args", "kwargs"), [ ( ("/my/path",), { "protocol": "file", "foo": "bar", "baz": 3, }, ) ], ) @pytest.mark.parametrize("mode", ["json", "python"]) def test_dump(args, kwargs, mode): u = UPath(*args, **kwargs) output = pydantic.TypeAdapter(UPath).dump_python(u, mode=mode) assert output["path"] == u.path assert output["protocol"] == u.protocol assert output["storage_options"] == u.storage_options def test_dump_non_serializable_python(): output = pydantic.TypeAdapter(UPath).dump_python( UPath("https://www.example.com", get_client=get_client), mode="python" ) assert output["storage_options"]["get_client"] is get_client def test_dump_non_serializable_json(): with pytest.raises(pydantic_core.PydanticSerializationError, match="unknown type"): pydantic.TypeAdapter(UPath).dump_python( UPath("https://www.example.com", get_client=get_client), mode="json" ) def test_proxyupath_serialization(): from upath.extensions import ProxyUPath u = ProxyUPath("memory://my/path", some_option=True) ta = pydantic.TypeAdapter(ProxyUPath) dumped = ta.dump_python(u, mode="python") loaded = ta.validate_python(dumped) assert isinstance(loaded, ProxyUPath) assert loaded.path == u.path assert loaded.protocol == u.protocol assert loaded.storage_options == u.storage_options @pytest.mark.parametrize( "path,cls", [ pytest.param("/my/path", PosixUPath, marks=skip_on_windows(None)), pytest.param("C:\\my\\path", WindowsUPath, marks=only_on_windows(None)), ("file:///my/path", FilePath), ], ) def test_localpath_serialization(path, cls): u = UPath(path) assert type(u) is cls ta = pydantic.TypeAdapter(cls) dumped = ta.dump_python(u, mode="python") loaded = ta.validate_python(dumped) assert isinstance(loaded, cls) assert loaded.path == u.path assert loaded.protocol == u.protocol assert loaded.storage_options == u.storage_options def test_json_schema(): ta = pydantic.TypeAdapter(UPath) ta.json_schema() universal_pathlib-0.3.10/upath/tests/test_registry.py000066400000000000000000000103151514661127100230560ustar00rootroot00000000000000import random import string import pytest from fsspec.implementations.local import LocalFileSystem from fsspec.registry import _registry as fsspec_registry_private from fsspec.registry import known_implementations as fsspec_known_implementations from fsspec.registry import register_implementation as fsspec_register_implementation from fsspec.registry import registry as fsspec_registry from upath import UPath from upath.registry import available_implementations from upath.registry import get_upath_class from upath.registry import register_implementation IMPLEMENTATIONS = { "abfs", "abfss", "adl", "az", "data", "file", "ftp", "gcs", "gs", "hdfs", "hf", "http", "https", "local", "memory", "s3", "s3a", "simplecache", "sftp", "smb", "ssh", "webdav", "webdav+http", "webdav+https", "github", "zip", "tar", } @pytest.fixture(autouse=True) def reset_registry(): from upath.registry import _registry try: yield finally: _registry._m.maps[0].clear() # type: ignore @pytest.fixture() def fake_entrypoint(): from importlib.metadata import EntryPoint from upath.registry import _registry ep = EntryPoint( name="myeps", value="upath.core:UPath", group="universal_pathlib.implementations", ) old_registry = _registry._entries.copy() try: _registry._entries["myeps"] = ep yield finally: _registry._entries.clear() _registry._entries.update(old_registry) def test_available_implementations(): impl = available_implementations() assert len(impl) == len(set(impl)) assert set(impl) == IMPLEMENTATIONS @pytest.fixture def fake_registered_proto(): fake_proto = "".join(random.choices(string.ascii_lowercase, k=8)) class FakeRandomFS(LocalFileSystem): protocol = fake_proto fsspec_register_implementation(fake_proto, FakeRandomFS) try: yield fake_proto finally: fsspec_registry_private.pop(fake_proto, None) def test_available_implementations_with_fallback(fake_registered_proto): impl = available_implementations(fallback=True) assert fake_registered_proto in impl assert set(impl) == IMPLEMENTATIONS.union( { *fsspec_known_implementations, *fsspec_registry, } ) def test_available_implementations_with_entrypoint(fake_entrypoint): impl = available_implementations() assert set(impl) == IMPLEMENTATIONS.union({"myeps"}) def test_register_implementation(): class MyProtoPath(UPath): pass register_implementation("myproto", MyProtoPath) assert get_upath_class("myproto") is MyProtoPath def test_register_implementation_wrong_input(): with pytest.raises(TypeError): register_implementation(None, UPath) # type: ignore with pytest.raises(ValueError): register_implementation("incorrect**protocol", UPath) with pytest.raises(ValueError): register_implementation("myproto", object, clobber=True) # type: ignore with pytest.raises(ValueError): register_implementation("file", UPath, clobber=False) assert set(available_implementations()) == IMPLEMENTATIONS @pytest.mark.parametrize("protocol", IMPLEMENTATIONS) def test_get_upath_class(protocol): upath_cls = get_upath_class("file") assert issubclass(upath_cls, UPath) def test_get_upath_class_without_implementation(clear_registry): with pytest.warns( UserWarning, match="UPath 'mock' filesystem not explicitly implemented." ): upath_cls = get_upath_class("mock") assert issubclass(upath_cls, UPath) def test_get_upath_class_without_implementation_no_fallback(clear_registry): assert get_upath_class("mock", fallback=False) is None def test_get_upath_class_unknown_protocol(clear_registry): assert get_upath_class("doesnotexist") is None def test_get_upath_class_from_entrypoint(fake_entrypoint): assert issubclass(get_upath_class("myeps"), UPath) @pytest.mark.parametrize( "protocol", [pytest.param("", id="empty-str"), pytest.param(None, id="none")] ) def test_get_upath_class_falsey_protocol(protocol): assert issubclass(get_upath_class(protocol), UPath) universal_pathlib-0.3.10/upath/tests/test_relative.py000066400000000000000000000575261514661127100230400ustar00rootroot00000000000000"""Tests for relative path functionality.""" import os import pickle import re import tempfile import warnings from pathlib import Path import pytest from upath import UPath @pytest.mark.parametrize( "protocol,storage_options,path,base", [ ("memory", {}, "memory:///foo/bar/baz.txt", "memory:///foo"), ("s3", {"anon": True}, "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo"), ("gcs", {"token": "anon"}, "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo"), ("http", {"something": 1}, "http://host/foo/bar/baz.txt", "http://host/foo"), ( "https", {}, "https://host/foo/bar/baz.txt", "https://host/", ), ], ) def test_protocol_storage_options_fs_preserved(protocol, storage_options, path, base): """Test that protocol and storage_options are preserved in relative paths.""" p = UPath(path, protocol=protocol, **storage_options) root = UPath(base, protocol=protocol, **storage_options) rel = p.relative_to(root) assert rel.protocol == protocol assert dict(**rel.storage_options) == storage_options assert isinstance(rel.fs, type(p.fs)) @pytest.mark.parametrize( "protocol,path,base", [ ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo"), ("ftp", "ftp://user:pass@host/foo/bar/baz.txt", "ftp://user:pass@host/foo"), ("http", "http://host/foo/bar/baz.txt", "http://host/foo"), ("https", "https://host/foo/bar/baz.txt", "https://host/foo"), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo"), ], ) def test_relative_urlpath_raises_without_cwd(protocol, path, base): rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) with pytest.raises( NotImplementedError, match=re.escape(f"{type(rel).__name__}.cwd() is unsupported"), ): rel.cwd() with pytest.raises( NotImplementedError, match=re.escape( f"fsspec paths can not be relative and" f" {type(rel).__name__}.cwd() is unsupported" ), ): _ = rel.path @pytest.mark.parametrize( "pth,base,rel", [ ("/foo/bar/baz.txt", "/foo", "bar/baz.txt"), ("/foo/bar/baz/qux.txt", "/foo/bar", "baz/qux.txt"), ("/foo/bar/baz/qux.txt", "/foo/bar/baz", "qux.txt"), ("/foo/bar/baz", "/foo/bar/baz", "."), ], ) @pytest.mark.parametrize( "protocol", [ "memory", "file", "", ], ) def test_basic_relative_path_creation(protocol, pth, base, rel): rel_pth = UPath(pth, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert not rel_pth.is_absolute() assert rel_pth.as_posix() == rel def test_relative_path_validation(): """Test validation of relative_to arguments.""" p = UPath("memory:///foo/bar") # Different protocols should fail with pytest.raises(ValueError, match="incompatible protocols"): p.relative_to(UPath("s3://bucket")) # Different storage options should fail with pytest.raises(ValueError, match="incompatible storage_options"): UPath("s3://bucket/file", anon=True).relative_to( UPath("s3://bucket", anon=False) ) def test_path_not_in_subpath(): """Test relative_to with paths that don't have a parent-child relationship.""" p = UPath("memory:///foo/bar") other = UPath("memory:///baz") with pytest.raises(ValueError, match="is not in the subpath of"): p.relative_to(other) def test_filesystem_operations_fail_without_cwd(): """Test that filesystem operations fail on relative paths when cwd()""" p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") rel = p.relative_to(root) # Memory filesystem doesn't implement cwd(), so these should fail with pytest.raises( NotImplementedError, match=re.escape( "fsspec paths can not be relative and MemoryPath.cwd() is unsupported" ), ): _ = rel.path with pytest.raises( NotImplementedError, match=re.escape( "fsspec paths can not be relative and MemoryPath.cwd() is unsupported" ), ): rel.exists() def test_filesystem_operations_work_with_cwd(): """Test that filesystem operations work on relative paths when cwd()""" with tempfile.TemporaryDirectory() as tmpdir: # Create test file structure test_dir = os.path.join(tmpdir, "testdir") os.makedirs(test_dir, exist_ok=True) test_file = os.path.join(test_dir, "testfile.txt") with open(test_file, "w") as f: f.write("test content") # Create paths abs_path = UPath(test_file) abs_dir = UPath(test_dir) rel_path = abs_path.relative_to(abs_dir) assert not rel_path.is_absolute() assert str(rel_path) == "testfile.txt" # Change to the test directory and try filesystem operations old_cwd = os.getcwd() try: os.chdir(test_dir) # These should work now since we're in the right directory full_path = rel_path.path assert "testfile.txt" in full_path # Test that the file exists assert rel_path.exists() finally: os.chdir(old_cwd) def test_pickling_relative_paths(): """Test that relative paths can be pickled and unpickled.""" p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") rel = p.relative_to(root) # Pickle and unpickle pickled = pickle.dumps(rel) unpickled = pickle.loads(pickled) assert str(rel) == str(unpickled) assert rel.is_absolute() == unpickled.is_absolute() assert rel._relative_base == unpickled._relative_base def test_with_segments_preserves_relative_state(): """Test that with_segments preserves the relative state.""" p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") rel = p.relative_to(root) # Create new path with different segments new_rel = rel.with_segments("memory:///foo/other/file.txt") # Should still be relative with same root assert not new_rel.is_absolute() assert new_rel._relative_base == rel._relative_base def test_relative_path_parts(): """Test that parts work correctly for relative paths.""" p = UPath("memory:///foo/bar/baz/qux.txt") root = UPath("memory:///foo") rel = p.relative_to(root) assert p.parts == root.parts + rel.parts def test_absolute_method_behavior(): """Test that absolute() returns the original absolute path.""" p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") rel = p.relative_to(root) with pytest.raises( NotImplementedError, match=re.escape("MemoryPath.cwd() is unsupported"), ): rel.absolute() def test_is_absolute_method(): """Test is_absolute() method on relative paths.""" p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") rel = p.relative_to(root) assert not rel.is_absolute() def test_relative_path_comparison(): """Test that relative paths can be compared.""" p1 = UPath("memory:///foo/bar/baz.txt") p2 = UPath("memory:///foo/bar/qux.txt") root = UPath("memory:///foo") rel1 = p1.relative_to(root) rel2 = p2.relative_to(root) # Compare string representations since .path requires cwd() for memory:// assert str(rel1) != str(rel2) assert rel1 != rel2 # Same relative path should be equal rel1_copy = p1.relative_to(root) assert str(rel1) == str(rel1_copy) assert rel1 == rel1_copy # Same relative path from different base should be equal rel3 = UPath("memory:///a/b/c.txt").relative_to(UPath("memory:///a")) rel4 = UPath("file:///x/b/c.txt").relative_to(UPath("file:///x")) assert str(rel3) == str(rel4) assert rel3 == rel4 def test_nonrelative_path_is_absolute(): """Test that normal (non-relative) paths return True for is_absolute().""" p = UPath("memory:///foo/bar/baz.txt") assert p.is_absolute() def test_s3_relative_paths(): """Test relative paths work with S3 URLs.""" p = UPath("s3://test_bucket/dir/file.txt") root = UPath("s3://test_bucket") rel = p.relative_to(root) assert not rel.is_absolute() assert str(rel) == "dir/file.txt" @pytest.fixture def rel_path(): p = UPath("memory:///foo/bar/baz.txt") root = UPath("memory:///foo") yield p.relative_to(root) def test_relative_path_as_uri(rel_path): with pytest.raises( ValueError, match=f"relative path can't be expressed as a {rel_path.protocol} URI", ): rel_path.as_uri() @pytest.mark.parametrize( "method_args", [ pytest.param(("absolute", ()), id="absolute"), pytest.param(("chmod", (0o777,)), id="chmod"), pytest.param(("cwd", ()), id="cwd"), pytest.param(("exists", ()), id="exists"), pytest.param(("glob", ("*.txt",)), id="glob"), pytest.param(("group", ()), id="group"), pytest.param(("is_dir", ()), id="is_dir"), pytest.param(("is_file", ()), id="is_file"), pytest.param(("is_symlink", ()), id="is_symlink"), pytest.param(("iterdir", ()), id="iterdir"), pytest.param(("lchmod", (0o777,)), id="lchmod"), pytest.param(("lstat", ()), id="lstat"), pytest.param(("mkdir", ()), id="mkdir"), pytest.param(("open", ()), id="open"), pytest.param(("owner", ()), id="owner"), pytest.param(("read_bytes", ()), id="read_bytes"), pytest.param(("read_text", ()), id="read_text"), pytest.param(("readlink", ()), id="readlink"), pytest.param(("rename", ("a/b/c",)), id="rename"), pytest.param(("replace", ("...",)), id="replace"), pytest.param(("resolve", ()), id="resolve"), pytest.param(("rglob", ("*.txt",)), id="rglob"), pytest.param(("rmdir", ()), id="rmdir"), pytest.param(("samefile", ("...",)), id="samefile"), pytest.param(("stat", ()), id="stat"), pytest.param(("symlink_to", ("...",)), id="symlink_to"), pytest.param(("touch", ()), id="touch"), pytest.param(("unlink", ()), id="unlink"), pytest.param(("write_bytes", (b"data",)), id="write_bytes"), pytest.param(("write_text", ("data",)), id="write_text"), ], ) def test_path_operations_disabled_without_cwd(rel_path, method_args): """UPaths without .cwd() implementation should not allow path operations.""" method, args = method_args with ( pytest.raises(NotImplementedError), warnings.catch_warnings(), ): # warnings are asserted in other tests warnings.simplefilter("ignore", UserWarning) # next only needs to be called for iterdir and glob/rglob # but the other raise already in the getattr call next(getattr(rel_path, method)(*args)) @pytest.mark.parametrize( "protocol,path,base", [ ("", "/foo/bar/baz.txt", "/foo"), ("file", "/foo/bar/baz.txt", "/foo"), ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo"), ("ftp", "ftp://user:pass@host/foo/bar/baz.txt", "ftp://user:pass@host/foo"), ("http", "http://host/foo/bar/baz.txt", "http://host/foo"), ("https", "https://host/foo/bar/baz.txt", "https://host/foo"), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo"), ], ) def test_drive_root_anchor_empty_for_relative_paths(protocol, path, base): rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert (rel.drive, rel.root, rel.anchor) == ("", "", "") @pytest.mark.parametrize( "protocol,path,base,expected_rel", [ ("", "/foo/bar/baz.txt", "/foo", "bar/baz.txt"), ("file", "/foo/bar/baz.txt", "/foo", "bar/baz.txt"), ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo", "bar/baz.txt"), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo", "bar/baz.txt"), ( "ftp", "ftp://user:pass@host/foo/bar/baz.txt", "ftp://user:pass@host/foo", "bar/baz.txt", ), ("http", "http://host/foo/bar/baz.txt", "http://host/foo", "bar/baz.txt"), ("https", "https://host/foo/bar/baz.txt", "https://host/foo", "bar/baz.txt"), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo", "bar/baz.txt"), ], ) def test_relative_path_properties(protocol, path, base, expected_rel): rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert not rel.is_absolute() assert rel.as_posix() == expected_rel assert rel.parts == tuple(expected_rel.split("/")) @pytest.mark.parametrize( "protocol,path,base,expected_parts", [ ("", "/foo/bar/baz.txt", "/foo", ("bar", "baz.txt")), ("file", "/foo/bar/baz.txt", "/foo", ("bar", "baz.txt")), ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo", ("bar", "baz.txt")), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo", ("bar", "baz.txt")), ( "ftp", "ftp://user:pass@host/foo/bar/baz.txt", "ftp://user:pass@host/foo", ("bar", "baz.txt"), ), ("http", "http://host/foo/bar/baz.txt", "http://host/foo", ("bar", "baz.txt")), ( "https", "https://host/foo/bar/baz.txt", "https://host/foo", ("bar", "baz.txt"), ), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo", ("bar", "baz.txt")), ], ) def test_relative_path_parts_property(protocol, path, base, expected_parts): rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert rel.parts == expected_parts def test_relative_path_is_something(rel_path): assert rel_path.is_block_device() is False assert rel_path.is_char_device() is False assert rel_path.is_fifo() is False assert rel_path.is_mount() is False assert rel_path.is_reserved() is False assert rel_path.is_socket() is False def test_relative_path_hashable(): x = UPath("memory:///a/b/c.txt") y = x.relative_to(UPath("memory:///a")) assert hash(y) != hash(x) def test_relative_path_expanduser_noop(rel_path): # this should be revisited if we ever add ~ support to non-file protocols assert rel_path == rel_path.expanduser() def test_relative_path_stem_suffix_name(rel_path): assert rel_path.name == "baz.txt" assert rel_path.stem == "baz" assert rel_path.suffix == ".txt" assert rel_path.suffixes == [".txt"] assert rel_path.with_name("other.txt").name == "other.txt" assert rel_path.with_stem("other").name == "other.txt" assert rel_path.with_suffix(".md").name == "baz.md" assert rel_path.with_suffix(".tar.gz").suffixes == [".tar", ".gz"] @pytest.mark.parametrize( "protocol,pth,base,expected_parent", [ ("", "/foo/bar/baz.txt", "/foo", "bar"), ("", "/foo", "/foo", "."), ("file", "/foo/bar/baz.txt", "/foo", "bar"), ("file", "/foo/bar", "/foo/bar", "."), ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo", "bar"), ("s3", "s3://bucket/foo/bar/", "s3://bucket/foo/bar", "."), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo", "bar"), ("gcs", "gcs://bucket/foo/bar/", "gcs://bucket/foo", "."), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo", "bar"), ("memory", "memory:///foo/bar", "memory:///foo", "."), ("https", "https://host/foo/bar/baz.txt", "https://host/foo", "bar"), ("https", "https://host/foo/bar/", "https://host/foo/bar", "."), ], ) def test_relative_path_parent(protocol, pth, base, expected_parent): rel = UPath(pth, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert str(rel.parent) == expected_parent @pytest.mark.parametrize( "uri,base,expected_parents_parts", [ ("/foo/bar/baz/qux.txt", "/foo", [("bar", "baz"), ("bar",), ()]), ("file:///foo/bar/baz/qux.txt", "file:///foo", [("bar", "baz"), ("bar",), ()]), ("s3://bucket/foo/bar/baz/", "s3://bucket/", [("foo", "bar"), ("foo",), ()]), ("gcs://bucket/foo/bar/baz", "gcs://bucket/", [("foo", "bar"), ("foo",), ()]), ("az://bucket/foo/bar/baz", "az://bucket/", [("foo", "bar"), ("foo",), ()]), ( "memory:///foo/bar/baz/qux.txt", "memory:///foo", [("bar", "baz"), ("bar",), ()], ), ( "https://host.com/foo/bar/baz/qux.txt", "https://host.com/foo", [("bar", "baz"), ("bar",), ()], ), ], ) def test_relative_path_parents(uri, base, expected_parents_parts): rel = UPath(uri).relative_to(UPath(base)) parents = list(rel.parents) assert [x.parts for x in parents] == expected_parents_parts @pytest.mark.parametrize( "protocol,pth,base", [ ("", "/foo/bar/baz.txt", "/foo"), ("file", "/foo/bar/baz.txt", "/foo"), ], ) def test_home_works_for_local_paths(protocol, pth, base): rel = UPath(pth, protocol=protocol).relative_to(UPath(base, protocol=protocol)) prefix = f"{protocol}://" if protocol else "" assert rel.home().as_posix() == prefix + UPath.home().as_posix() @pytest.mark.parametrize( "protocol,pth,base", [ ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo"), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo"), ("https", "https://host/foo/bar/baz.txt", "https://host/foo"), ], ) def test_home_raises_for_non_local_paths(protocol, pth, base): rel = UPath(pth, protocol=protocol).relative_to(UPath(base, protocol=protocol)) with pytest.raises( NotImplementedError, match=re.escape(f"{type(rel).__name__}.home() is unsupported"), ): rel.home() @pytest.mark.parametrize( "protocol,pth,base", [ ("", "/foo/bar/baz.txt", "/foo"), ("file", "/foo/bar/baz.txt", "/foo"), ("s3", "s3://bucket/foo/bar/baz.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz.txt", "gcs://bucket/foo"), ("memory", "memory:///foo/bar/baz.txt", "memory:///foo"), ("https", "https://host/foo/bar/baz.txt", "https://host/foo"), ], ) def test_parser_attribute_available(protocol, pth, base): rel_path = UPath(pth, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert rel_path.parser is not None @pytest.mark.parametrize( "protocol", [ "", "file", ], ) def test_relpath_path_resolve(tmp_path, protocol, monkeypatch): """This should work for all path types that support .cwd()""" base = UPath(tmp_path, protocol=protocol) (base / "a" / "b").mkdir(parents=True) (base / "a" / "b" / "file.txt").write_text("data") monkeypatch.chdir(base) rel = UPath("/xyz/a/b/c/d/../../file.txt", protocol=protocol).relative_to( UPath("/xyz", protocol=protocol) ) assert rel.as_posix() == "a/b/c/d/../../file.txt" resolved = rel.resolve() prefix = f"{protocol}://" if protocol else "" assert ( resolved.as_posix() == prefix + (tmp_path / "a" / "b" / "file.txt").as_posix() ) assert resolved.read_text() == "data" assert resolved.is_absolute() assert resolved.exists() @pytest.mark.parametrize( "protocol,path,base", [ ("", "/foo/bar/baz/qux.txt", "/foo"), ("file", "/foo/bar/baz/qux.txt", "/foo"), ("s3", "s3://bucket/foo/bar/baz/qux.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz/qux.txt", "gcs://bucket/foo"), ("memory", "memory:///foo/bar/baz/qux.txt", "memory:///foo"), ("https", "https://host/foo/bar/baz/qux.txt", "https://host/foo"), ], ) def test_relative_path_match(protocol, path, base): """Test that match works correctly for relative paths.""" rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) assert rel.as_posix() == "bar/baz/qux.txt" # Should match patterns that match the relative path assert rel.match("bar/baz/qux.txt") assert rel.match("*/baz/qux.txt") assert rel.match("bar/*/qux.txt") assert rel.match("*/**/*.txt") # ** acts like * # Should not match patterns that don't match assert not rel.match("foo/baz/qux.txt") assert not rel.match("*.py") assert not rel.match("other.txt") @pytest.mark.parametrize( "protocol,path,base", [ ("", "/foo/bar/baz/qux.txt", "/foo"), ("file", "/foo/bar/baz/qux.txt", "/foo"), ("s3", "s3://bucket/foo/bar/baz/qux.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz/qux.txt", "gcs://bucket/foo"), ("memory", "memory:///foo/bar/baz/qux.txt", "memory:///foo"), ("https", "https://host/foo/bar/baz/qux.txt", "https://host/foo"), ], ) def test_relative_path_joinpath(protocol, path, base): """Test that joinpath works correctly for relative paths.""" rel = UPath(path, protocol=protocol).relative_to(UPath(base, protocol=protocol)) # Test joining with a single segment assert rel.as_posix() == "bar/baz/qux.txt" joined = rel.joinpath("extra.txt") assert joined.as_posix() == "bar/baz/qux.txt/extra.txt" assert not joined.is_absolute() # Test joining with multiple segments joined_multi = rel.joinpath("dir", "file.py") assert joined_multi.as_posix() == "bar/baz/qux.txt/dir/file.py" assert not joined_multi.is_absolute() # Test that the result is still relative with same base assert joined.protocol == joined_multi.protocol == protocol assert joined.storage_options == joined_multi.storage_options == rel.storage_options @pytest.mark.parametrize( "protocol,path,base", [ ("", "/foo/bar/baz/qux.txt", "/foo"), ("file", "/foo/bar/baz/qux.txt", "/foo"), ("s3", "s3://bucket/foo/bar/baz/qux.txt", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar/baz/qux.txt", "gcs://bucket/foo"), ("memory", "memory:///foo/bar/baz/qux.txt", "memory:///foo"), ("https", "https://host/foo/bar/baz/qux.txt", "https://host/foo"), ], ) def test_join_local_absolute_path_to_relative(protocol, path, base, tmp_path): """Test that joining an absolute path to a relative path works correctly.""" rel = UPath(path, protocol=protocol).relative_to(base) assert rel.as_posix() == "bar/baz/qux.txt" tmp_path.joinpath("bar/baz/qux.txt").parent.mkdir(parents=True, exist_ok=True) tmp_path.joinpath("bar/baz/qux.txt").write_text("data") assert UPath(tmp_path).joinpath(rel).read_text() == "data" @pytest.mark.parametrize( "protocol,path", [ ("", "/foo/bar"), ("file", "/foo/bar"), ("s3", "s3://bucket/foo/bar"), ("gcs", "gcs://bucket/foo/bar"), ("memory", "memory:///foo/bar"), ("https", "https://host/foo/bar"), ], ) def test_join_fsspec_absolute_path_to_relative(protocol, path): p = UPath(path, protocol=protocol) x = p.joinpath(Path("a/b/c").as_posix()) assert x.path.endswith("foo/bar/a/b/c") @pytest.mark.parametrize( "proto0,path0", [ ("", "/foo/bar"), ("file", "/foo/bar"), ("s3", "s3://bucket/foo/bar"), ("gcs", "gcs://bucket/foo/bar"), ("memory", "memory:///foo/bar"), ("https", "https://host/foo/bar"), ], ) @pytest.mark.parametrize( "proto1,path1,base1", [ ("", "/foo/bar", "/foo"), ("file", "/foo/bar", "/foo"), ("s3", "s3://bucket/foo/bar", "s3://bucket/foo"), ("gcs", "gcs://bucket/foo/bar", "gcs://bucket/foo"), ("memory", "memory:///foo/bar", "memory:///foo"), ("https", "https://host/foo/bar", "https://host/foo"), ], ) def test_join_fsspec_absolute_path_to_fsspec_relative( proto0, path0, proto1, path1, base1 ): p0 = UPath(path0, protocol=proto0) p1 = UPath(path1, protocol=proto1).relative_to(base1) assert str(p1) == "bar" x = p0.joinpath(p1) assert x.path.endswith("foo/bar/bar") universal_pathlib-0.3.10/upath/tests/test_stat.py000066400000000000000000000060451514661127100221660ustar00rootroot00000000000000import os from datetime import datetime from datetime import timezone import pytest import upath @pytest.fixture def pth_file(tmp_path): f = tmp_path.joinpath("abc.txt") f.write_bytes(b"a") p = upath.UPath(f"file://{f.absolute().as_posix()}") yield p def test_stat_repr(pth_file): assert repr(pth_file.stat()).startswith("UPathStatResult") def test_stat_as_info(pth_file): dct = pth_file.stat().as_info() assert dct["size"] == pth_file.stat().st_size def test_stat_atime(pth_file): atime = pth_file.stat().st_atime assert isinstance(atime, (float, int)) @pytest.mark.xfail(reason="fsspec does not return 'atime'") def test_stat_atime_value(pth_file): atime = pth_file.stat().st_atime assert atime > 0 def test_stat_mtime(pth_file): mtime = pth_file.stat().st_mtime assert isinstance(mtime, (float, int)) def test_stat_mtime_value(pth_file): mtime = pth_file.stat().st_mtime assert mtime > 0 def test_stat_ctime(pth_file): ctime = pth_file.stat().st_ctime assert isinstance(ctime, (float, int)) @pytest.mark.xfail(reason="fsspec returns 'created' but not 'ctime'") def test_stat_ctime_value(pth_file): ctime = pth_file.stat().st_ctime assert ctime > 0 def test_stat_birthtime(pth_file): birthtime = pth_file.stat().st_birthtime assert isinstance(birthtime, (float, int)) def test_stat_birthtime_value(pth_file): birthtime = pth_file.stat().st_birthtime assert birthtime > 0 def test_stat_seq_interface(pth_file): assert len(tuple(pth_file.stat())) == os.stat_result.n_sequence_fields assert isinstance(pth_file.stat().index(0), int) assert isinstance(pth_file.stat().count(0), int) assert isinstance(pth_file.stat()[0], int) def test_stat_warn_if_dict_interface(pth_file): with pytest.warns(DeprecationWarning): pth_file.stat().keys() with pytest.warns(DeprecationWarning): pth_file.stat().items() with pytest.warns(DeprecationWarning): pth_file.stat().values() with pytest.warns(DeprecationWarning): pth_file.stat().get("size") with pytest.warns(DeprecationWarning): pth_file.stat().copy() with pytest.warns(DeprecationWarning): _ = pth_file.stat()["size"] @pytest.mark.parametrize( "timestamp", [ 10, datetime(1970, 1, 1, 0, 0, 10, tzinfo=timezone.utc), "1970-01-01T00:00:10Z", "1970-01-01T00:00:10+00:00", ], ) def test_timestamps(timestamp): from upath._stat import UPathStatResult s = UPathStatResult( [0] * 10, { "ctime": timestamp, "atime": timestamp, "mtime": timestamp, "created": timestamp, }, ) assert s.st_atime == 10.0 assert s.st_ctime == 10.0 assert s.st_mtime == 10.0 def test_bad_timestamp(): from upath._stat import UPathStatResult with ( pytest.raises(TypeError), pytest.warns(RuntimeWarning, "universal_pathlib/issues"), ): s = UPathStatResult([0] * 10, {"ctime": "bad"}) _ = s.st_ctime universal_pathlib-0.3.10/upath/tests/third_party/000077500000000000000000000000001514661127100221265ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/third_party/__init__.py000066400000000000000000000000001514661127100242250ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/tests/third_party/test_pydantic.py000066400000000000000000000006631514661127100253570ustar00rootroot00000000000000import pytest try: from pydantic import BaseConfig from pydantic_settings import BaseSettings except ImportError: BaseConfig = BaseSettings = None pytestmark = pytest.mark.skip(reason="requires pydantic") from upath.core import UPath def test_pydantic_settings_local_upath(): class MySettings(BaseSettings): example_path: UPath = UPath(__file__) assert isinstance(MySettings().example_path, UPath) universal_pathlib-0.3.10/upath/tests/utils.py000066400000000000000000000111751514661127100213140ustar00rootroot00000000000000import operator import sys from contextlib import contextmanager import pytest from fsspec.utils import get_package_version_without_import from packaging.version import Version def skip_on_windows(func): return pytest.mark.skipif( sys.platform.startswith("win"), reason="Don't run on Windows" )(func) def only_on_windows(func): return pytest.mark.skipif( not sys.platform.startswith("win"), reason="Only run on Windows" )(func) def posixify(path): return str(path).replace("\\", "/") def xfail_if_version(module, *, reason, **conditions): ver_str = get_package_version_without_import(module) if ver_str is None: return pytest.mark.skip(reason=f"NOT INSTALLED ({reason})") ver = Version(ver_str) if not set(conditions).issubset({"lt", "le", "ne", "eq", "ge", "gt"}): raise ValueError("unknown condition") cond = True for op, val in conditions.items(): cond &= getattr(operator, op)(ver, Version(val)) return pytest.mark.xfail(cond, reason=reason) def xfail_if_no_ssl_connection(func): try: import requests except ImportError: return pytest.mark.skip(reason="requests not installed")(func) try: requests.get("https://example.com") except (requests.exceptions.ConnectionError, requests.exceptions.SSLError): return pytest.mark.xfail(reason="No SSL connection")(func) else: return func @contextmanager def temporary_register(protocol, cls): """helper to temporarily register a protocol for testing purposes""" from upath.registry import _registry from upath.registry import get_upath_class m = _registry._m.maps[0] try: m[protocol] = cls get_upath_class.cache_clear() yield finally: m.clear() get_upath_class.cache_clear() def extends_base(method): """Decorator to ensure a method extends the base class and does NOT override a method in base classes. Use this decorator in implementation-specific test classes to ensure that test methods don't accidentally override methods defined in test base classes. Example: class TestSpecificImpl(TestBaseClass, metaclass=OverrideMeta): @extends_base def test_something(self): # Raises TypeError if base has this method ... @extends_base def test_new_method(self): # This is fine - no override ... """ method.__override_check__ = False return method def overrides_base(method): """Decorator to ensure a method DOES override a method in base classes. Use this decorator in implementation-specific test classes to ensure that test methods intentionally override methods defined in test base classes. Example: class TestSpecificImpl(TestBaseClass, metaclass=OverrideMeta): @overrides_base def test_something(self): # Raises TypeError if base lacks this method ... @overrides_base def test_new_method(self): # Raises TypeError - no method to override ... """ method.__override_check__ = True return method class OverrideMeta(type): """Metaclass that enforces @extends_base and @overrides_base decorator constraints. When a class uses this metaclass: - Methods decorated with @extends_base are checked to ensure they don't override a method from any base class. - Methods decorated with @overrides_base are checked to ensure they do override a method from at least one base class. """ def __new__(mcs, name, bases, namespace): for attr_name, attr_value in namespace.items(): if not callable(attr_value): continue check = getattr(attr_value, "__override_check__", None) if check is None: continue has_in_base = any(hasattr(base, attr_name) for base in bases) if check is False and has_in_base: base_name = next(b.__name__ for b in bases if hasattr(b, attr_name)) raise TypeError( f"Method '{attr_name}' in class '{name}' is decorated " f"with @extends_base but overrides a method from base " f"class '{base_name}'" ) elif check is True and not has_in_base: raise TypeError( f"Method '{attr_name}' in class '{name}' is decorated " f"with @overrides_base but does not override any method from " f"base classes" ) return super().__new__(mcs, name, bases, namespace) universal_pathlib-0.3.10/upath/types/000077500000000000000000000000001514661127100175775ustar00rootroot00000000000000universal_pathlib-0.3.10/upath/types/__init__.py000066400000000000000000000070301514661127100217100ustar00rootroot00000000000000from __future__ import annotations import enum import sys from collections.abc import Callable from os import PathLike from typing import TYPE_CHECKING from typing import Any from typing import Optional from typing import Protocol from typing import Union from typing import runtime_checkable from upath.types._abc import JoinablePath from upath.types._abc import PathInfo from upath.types._abc import PathParser from upath.types._abc import ReadablePath from upath.types._abc import WritablePath if TYPE_CHECKING: if sys.version_info >= (3, 12): from typing import TypeAlias else: from typing_extensions import TypeAlias __all__ = [ "JoinablePath", "ReadablePath", "WritablePath", "JoinablePathLike", "ReadablePathLike", "WritablePathLike", "SupportsPathLike", "PathInfo", "StatResultType", "PathParser", "UPathParser", "UNSET_DEFAULT", "OnNameCollisionFunc", ] class VFSPathLike(Protocol): def __vfspath__(self) -> str: ... SupportsPathLike: TypeAlias = Union[VFSPathLike, PathLike[str]] JoinablePathLike: TypeAlias = Union[JoinablePath, SupportsPathLike, str] ReadablePathLike: TypeAlias = Union[ReadablePath, SupportsPathLike, str] WritablePathLike: TypeAlias = Union[WritablePath, SupportsPathLike, str] class _DefaultValue(enum.Enum): UNSET = enum.auto() UNSET_DEFAULT: Any = _DefaultValue.UNSET # We can't assume this, because pathlib_abc==0.5.1 is ahead of stdlib 3.14 # if sys.version_info >= (3, 14): # JoinablePath.register(pathlib.PurePath) # ReadablePath.register(pathlib.Path) # WritablePath.register(pathlib.Path) @runtime_checkable class StatResultType(Protocol): """duck-type for os.stat_result""" @property def st_mode(self) -> int: ... @property def st_ino(self) -> int: ... @property def st_dev(self) -> int: ... @property def st_nlink(self) -> int: ... @property def st_uid(self) -> int: ... @property def st_gid(self) -> int: ... @property def st_size(self) -> int: ... @property def st_atime(self) -> float: ... @property def st_mtime(self) -> float: ... @property def st_ctime(self) -> float: ... @property def st_atime_ns(self) -> int: ... @property def st_mtime_ns(self) -> int: ... @property def st_ctime_ns(self) -> int: ... # st_birthtime is available on Windows (3.12+), FreeBSD, and macOS # On Linux it's currently unavailable # see: https://discuss.python.org/t/st-birthtime-not-available/104350/2 if (sys.platform == "win32" and sys.version_info >= (3, 12)) or ( sys.platform == "darwin" or sys.platform.startswith("freebsd") ): @property def st_birthtime(self) -> float: ... @runtime_checkable class UPathParser(PathParser, Protocol): """duck-type for upath.core.UPathParser""" def split(self, path: JoinablePathLike) -> tuple[str, str]: ... def splitext(self, path: JoinablePathLike) -> tuple[str, str]: ... def normcase(self, path: JoinablePathLike) -> str: ... def strip_protocol(self, path: JoinablePathLike) -> str: ... def join( self, path: JoinablePathLike, *paths: JoinablePathLike, ) -> str: ... def isabs(self, path: JoinablePathLike) -> bool: ... def splitdrive(self, path: JoinablePathLike) -> tuple[str, str]: ... def splitroot(self, path: JoinablePathLike) -> tuple[str, str, str]: ... OnNameCollisionFunc: TypeAlias = Callable[ [ReadablePath, WritablePath], tuple[Optional[WritablePath], Optional[WritablePath]] ] universal_pathlib-0.3.10/upath/types/_abc.py000066400000000000000000000006711514661127100210410ustar00rootroot00000000000000"""pathlib_abc exports for compatibility with pathlib.""" from pathlib_abc import JoinablePath from pathlib_abc import PathInfo from pathlib_abc import PathParser from pathlib_abc import ReadablePath from pathlib_abc import WritablePath from pathlib_abc import vfsopen from pathlib_abc import vfspath __all__ = [ "JoinablePath", "ReadablePath", "WritablePath", "PathInfo", "PathParser", "vfsopen", "vfspath", ] universal_pathlib-0.3.10/upath/types/_abc.pyi000066400000000000000000000104511514661127100212070ustar00rootroot00000000000000"""pathlib_abc exports for compatibility with pathlib.""" import sys from abc import ABC from abc import abstractmethod from typing import Any from typing import BinaryIO from typing import Callable from typing import Iterator from typing import Literal from typing import Protocol from typing import Sequence from typing import TextIO from typing import TypeVar from typing import runtime_checkable if sys.version_info > (3, 11): from typing import Self else: from typing_extensions import Self class JoinablePath(ABC): __slots__ = () @property @abstractmethod def parser(self) -> PathParser: ... @abstractmethod def with_segments(self, *pathsegments: str) -> Self: ... @abstractmethod def __vfspath__(self) -> str: ... @property def anchor(self) -> str: ... @property def name(self) -> str: ... @property def suffix(self) -> str: ... @property def suffixes(self) -> list[str]: ... @property def stem(self) -> str: ... def with_name(self, name: str) -> Self: ... def with_stem(self, stem: str) -> Self: ... def with_suffix(self, suffix: str) -> Self: ... @property def parts(self) -> Sequence[str]: ... def joinpath(self, *pathsegments: str) -> Self: ... def __truediv__(self, key: str) -> Self: ... def __rtruediv__(self, key: str) -> Self: ... @property def parent(self) -> Self: ... @property def parents(self) -> Sequence[Self]: ... def full_match(self, pattern: str) -> bool: ... OnErrorCallable = Callable[[Exception], Any] T = TypeVar("T", bound="WritablePath") class ReadablePath(JoinablePath): __slots__ = () @property @abstractmethod def info(self) -> PathInfo: ... @abstractmethod def __open_reader__(self) -> BinaryIO: ... def read_bytes(self) -> bytes: ... def read_text( self, encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., ) -> str: ... @abstractmethod def iterdir(self) -> Iterator[Self]: ... def glob(self, pattern: str, *, recurse_symlinks: bool = ...) -> Iterator[Self]: ... def walk( self, top_down: bool = ..., on_error: OnErrorCallable | None = ..., follow_symlinks: bool = ..., ) -> Iterator[tuple[Self, list[str], list[str]]]: ... @abstractmethod def readlink(self) -> Self: ... def copy(self, target: T, **kwargs: Any) -> T: ... def copy_into(self, target_dir: T, **kwargs: Any) -> T: ... class WritablePath(JoinablePath): __slots__ = () @abstractmethod def symlink_to( self, target: ReadablePath, target_is_directory: bool = ... ) -> None: ... @abstractmethod def mkdir(self) -> None: ... @abstractmethod def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: ... def write_bytes(self, data: bytes) -> int: ... def write_text( self, data: str, encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., ) -> int: ... def _copy_from(self, source: ReadablePath, follow_symlinks: bool = ...) -> None: ... @runtime_checkable class PathParser(Protocol): sep: str altsep: str | None def split(self, path: str) -> tuple[str, str]: ... def splitext(self, path: str) -> tuple[str, str]: ... def normcase(self, path: str) -> str: ... @runtime_checkable class PathInfo(Protocol): def exists(self, *, follow_symlinks: bool = True) -> bool: ... def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... class SupportsOpenReader(Protocol): def __open_reader__(self) -> BinaryIO: ... class SupportsOpenWriter(Protocol): def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: ... class SupportsOpenUpdater(Protocol): def __open_updater__(self, mode: Literal["r+", "w+", "+r", "+w"]) -> BinaryIO: ... def vfsopen( obj: SupportsOpenReader | SupportsOpenWriter | SupportsOpenUpdater, mode="r", buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> BinaryIO | TextIO: ... class SupportsVFSPath(Protocol): def __vfspath__(self) -> str: ... def vfspath(obj: SupportsVFSPath) -> str: ... universal_pathlib-0.3.10/upath/types/storage_options.py000066400000000000000000000342071514661127100233760ustar00rootroot00000000000000"""Storage options types for various filesystems based on fsspec.""" from __future__ import annotations from typing import TYPE_CHECKING from typing import TypedDict if TYPE_CHECKING: from asyncio import AbstractEventLoop from typing import Any from typing import Literal from fsspec.implementations.cache_mapper import AbstractCacheMapper from fsspec.spec import AbstractFileSystem __all__ = [ "SimpleCacheStorageOptions", "GCSStorageOptions", "S3StorageOptions", "AzureStorageOptions", "HfStorageOptions", "DataStorageOptions", "FTPStorageOptions", "GitHubStorageOptions", "HDFSStorageOptions", "HTTPStorageOptions", "FileStorageOptions", "MemoryStorageOptions", "SFTPStorageOptions", "SMBStorageOptions", "WebdavStorageOptions", "ZipStorageOptions", "TarStorageOptions", ] class _AbstractStorageOptions(TypedDict, total=False): """Base storage options for fsspec-based filesystems""" # dircache related options use_listings_cache: bool listings_expiry_time: int | float | None max_paths: int | None # fs instance cache options skip_instance_cache: bool # async fs instance options asynchronous: bool loop: AbstractEventLoop | None batch_size: int | None class _ChainableStorageOptions(TypedDict, total=False): """Storage options for filesystems supporting chaining""" target_protocol: str | None target_options: dict[str, Any] | None fs: AbstractFileSystem | None class SimpleCacheStorageOptions( _AbstractStorageOptions, _ChainableStorageOptions, total=False, ): """Storage options for SimpleCache""" cache_storage: Literal["TMP"] | list[str] | str cache_check: int | Literal[False] check_files: bool expiry_time: int | Literal[False] same_names: bool | None compression: str # todo: specify allowed values cache_mapper: AbstractCacheMapper | None class GCSStorageOptions(_AbstractStorageOptions, total=False): """Storage options for Google Cloud Storage""" # Authentication and project settings project: str access: Literal["read_only", "read_write", "full_control"] token: ( None | Literal["google_default", "cache", "anon", "browser", "cloud"] | str | dict[str, Any] ) # Performance and caching block_size: int | None consistency: Literal["none", "size", "md5"] cache_timeout: float | None # overrides listings_expiry_time if set # Request configuration requests_timeout: float | None requester_pays: bool | str session_kwargs: dict[str, Any] | None # aiohttp.ClientSession kwargs timeout: float | None # timeout used for .buckets? # Connection settings endpoint_url: str | None check_connection: bool | None # Storage configuration default_location: str | None version_aware: bool # Deprecated options # secure_serialize: bool class S3StorageOptions(_AbstractStorageOptions, total=False): """Storage options for AWS S3 and S3-compatible services""" # Authentication anon: bool key: str | None secret: str | None token: str | None username: str | None # alias for key password: str | None # alias for secret # Connection settings endpoint_url: str | None use_ssl: bool # AWS-specific configuration client_kwargs: dict[str, Any] | None # botocore Client kwargs config_kwargs: dict[str, Any] | None # botocore Config kwargs s3_additional_kwargs: dict[str, Any] | None # s3 api methods kwargs session: Any | None # aiobotocore AioSession # Performance settings requester_pays: bool default_block_size: int | None default_fill_cache: bool default_cache_type: str # fsspec.caching Literal["readahead", "none", "bytes", ...] max_concurrency: int fixed_upload_size: bool # Feature flags version_aware: bool cache_regions: bool class AzureStorageOptions(_AbstractStorageOptions, total=False): """Storage options for Azure Blob Storage and Azure Data Lake Gen2""" # Account and authentication account_name: str | None account_key: str | None connection_string: str | None credential: ( str | Any | None ) # azure.core.credentials_async.AsyncTokenCredential or SAS token sas_token: str | None anon: bool | None # Service Principal authentication client_id: str | None client_secret: str | None tenant_id: str | None # Connection and networking # request_session: Any | None # for http requests not used by anything ??? # socket_timeout: int | None deprecated account_host: str | None location_mode: Literal["primary", "secondary"] # Performance settings blocksize: int # block size for download/upload operations default_fill_cache: bool default_cache_type: str # fsspec cache type max_concurrency: int | None # Timeout settings timeout: int | None # server-side timeout for operations connection_timeout: int | None # connection establishment timeout read_timeout: int | None # read operation timeout # Feature flags version_aware: bool # support blob versioning assume_container_exists: bool | None # container existence assumptions class HfStorageOptions(_AbstractStorageOptions, total=False): """Storage options for Hugging face filesystem""" # Authentication token: str | None # Connection settings endpoint: str | None # Performance settings block_size: ( int | None ) # Block size for reading bytes; 0 = raw requests file-like objects class DataStorageOptions(_AbstractStorageOptions, total=False): """Storage options for Data URIs filesystem""" # No specific options for Data URIs at the moment class FTPStorageOptions(_AbstractStorageOptions, total=False): """Storage options for FTP filesystem""" # Connection settings host: str # The remote server name/ip to connect to (required) port: int # Port to connect with (default: 21) # Authentication username: ( str | None ) # User's identifier for authentication (anonymous if not given) password: str | None # User's password on the server acct: str | None # Account string for authentication (some servers require this) # Performance settings block_size: int | None # Read-ahead or write buffer size # FTP-specific settings tempdir: ( str | None ) # Directory on remote to put temporary files when in a transaction timeout: int # Timeout of the FTP connection in seconds (default: 30) encoding: str # Encoding for dir and filenames in FTP connection (default: "utf-8") # Security settings tls: bool # Use FTP-TLS (default: False) class GitHubStorageOptions(_AbstractStorageOptions, total=False): """Storage options for GitHub repository filesystem""" # Repository identification org: str # GitHub organization or username repo: str # Repository name sha: str | None # Commit SHA, branch, or tag (default: current master) # Authentication username: str | None # GitHub username for authenticated access token: str | None # GitHub personal access token # Connection settings timeout: tuple[int, int] | int | None # (connect, read) timeouts or single timeout class HDFSStorageOptions(_AbstractStorageOptions, total=False): """Storage options for Hadoop Distributed File System (HDFS)""" # Connection settings host: str # Hostname, IP or "default" to try to read from Hadoop config port: int # Port to connect on, or default from Hadoop config if 0 # Authentication user: str | None # Username to connect as kerb_ticket: str | None # Kerberos ticket for authentication # HDFS-specific settings replication: int # Replication factor for write operations (default: 3) extra_conf: ( dict[str, Any] | None ) # Additional configuration passed to HadoopFileSystem class HTTPStorageOptions(_AbstractStorageOptions, total=False): """Storage options for HTTP(S) filesystem""" # Performance settings block_size: ( int | None ) # Block size for reading bytes; 0 = raw requests file-like objects # Link parsing behavior simple_links: ( bool # Consider both HTML tags and URL-like strings vs HTML tags only ) same_scheme: ( bool # Only consider paths with matching http/https scheme during ls/glob ) # Caching configuration cache_type: str # Default cache type used in open() (e.g., "bytes") cache_options: dict[str, Any] | None # Default cache options used in open() # HTTP client configuration client_kwargs: dict[str, Any] | None # Passed to aiohttp.ClientSession get_client: Any | None # Callable that constructs aiohttp.ClientSession # Encoding settings encoded: bool # Whether URLs should be encoded # Deprecated options # size_policy: Any # Deprecated parameter class FileStorageOptions(_AbstractStorageOptions, total=False): """Storage options for local filesystem (file:// and local:// protocols)""" # File system behavior auto_mkdir: bool # Whether to create parent directories when opening files class MemoryStorageOptions(_AbstractStorageOptions, total=False): """Storage options for memory filesystem""" # No specific options for memory filesystem at the moment class SFTPStorageOptions(_AbstractStorageOptions, total=False): """Storage options for SFTP/SSH filesystem""" # Connection settings host: str # Hostname or IP address (required) port: int | None # SSH port (default: 22) # Authentication username: str | None # Username to authenticate as password: ( str | None ) # Password authentication; also used for private key decryption passphrase: str | None # Used for decrypting private keys pkey: Any | None # Private key for authentication (paramiko.PKey) key_filename: str | list[str] | None # Filename(s) of private key(s)/certs to try # Connection behavior timeout: float | None # TCP connect timeout in seconds allow_agent: bool | None # Whether to connect to SSH agent (default: True) look_for_keys: ( bool | None ) # Whether to search for private keys in ~/.ssh/ (default: True) compress: bool | None # Whether to turn on compression sock: Any | None # Socket or socket-like object for communication # GSS-API authentication gss_auth: bool | None # Use GSS-API authentication gss_kex: bool | None # Perform GSS-API Key Exchange and user authentication gss_deleg_creds: bool | None # Delegate GSS-API client credentials gss_host: str | None # Target name in kerberos database (default: hostname) gss_trust_dns: bool | None # Trust DNS to canonicalize hostname (default: True) # Timeout settings banner_timeout: float | None # SSH banner timeout in seconds auth_timeout: float | None # Authentication response timeout in seconds channel_timeout: float | None # Channel open response timeout in seconds # Advanced configuration disabled_algorithms: ( dict[str, Any] | None ) # Algorithms to disable (passed to Transport) transport_factory: Any | None # Callable to generate Transport instance auth_strategy: Any | None # AuthStrategy instance for newer authentication # SFTP-specific settings temppath: str # Remote temporary directory path (default: "/tmp") class SMBStorageOptions(_AbstractStorageOptions, total=False): """Storage options for SMB/Windows/Samba network shares""" # Connection settings host: str # The remote server name/ip to connect to (required) port: int | None # Port to connect with (usually 445, sometimes 139) # Authentication username: str | None # Username to connect with (required if not using Kerberos) password: str | None # User's password on the server # Connection behavior timeout: int # Connection timeout in seconds (default: 60) encrypt: bool | None # Whether to force encryption # File access control share_access: Literal["r", "w", "d"] | None # Default access for file operations # None (default): exclusively locks file until closed # 'r': Allow other handles with read access # 'w': Allow other handles with write access # 'd': Allow other handles with delete access # Session retry configuration register_session_retries: int # Number of session registration retries (default: 4) register_session_retry_wait: ( int # Wait time between retries in seconds (default: 1) ) register_session_retry_factor: int # Exponential backoff factor (default: 10) # File system behavior auto_mkdir: bool # Whether to create parent directories when opening files class WebdavStorageOptions(_AbstractStorageOptions, total=False): """Storage options for WebDAV filesystem""" # Connection settings base_url: str # Base URL of the WebDAV server (required) # Authentication auth: ( tuple[str, str] | Any | None ) # Authentication (username, password) tuple or custom auth # Client configuration client: Any | None # webdav4.client.Client instance class ZipStorageOptions( _AbstractStorageOptions, _ChainableStorageOptions, total=False, ): """Storage options for ZIP archive filesystem""" # Archive file settings fo: str | Any # Path to ZIP file or file-like object mode: Literal["r", "w", "a"] # Open mode: read, write, or append # ZIP compression settings compression: int # Compression method (e.g., zipfile.ZIP_STORED, ZIP_DEFLATED) allowZip64: bool # Enable ZIP64 extensions for large files compresslevel: int | None # Compression level (None uses default for method) class TarStorageOptions( _AbstractStorageOptions, _ChainableStorageOptions, total=False, ): """Storage options for TAR archive filesystem (read-only)""" # Archive file settings fo: str | Any # Path to TAR file or file-like object # Compression settings compression: ( str | None ) # Compression method: 'gzip', 'bz2', 'xz', or None for auto-detect index_store: str | None # Path to store/load the file index cache