├── .coveragerc ├── .github └── workflows │ ├── python-package.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── VERSION ├── codecov.yml ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── mypy.ini ├── pytest.ini ├── setup.py ├── src └── pathlib2 │ ├── __init__.py │ ├── _ntpath.py │ └── _posixpath.py └── tests ├── __init__.py ├── os_helper.py ├── requirements.txt ├── test_pathlib2.py ├── test_private.py └── test_unicode.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = tests,pathlib2 4 | omit = tests/os_helper.py 5 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.platform }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | platform: ['ubuntu-latest', 'windows-latest', 'macos-latest'] 17 | python-version: ['3.8', '3.9', '3.10', '3.11'] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install -r tests/requirements.txt 28 | python -m pip install flake8 check-manifest pytest codecov coverage 29 | python -m pip install . 30 | - name: Check manifest 31 | run: check-manifest 32 | - name: Lint with flake8 33 | run: | 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source 35 | flake8 . --count --exit-zero --max-complexity=10 36 | - name: Documentation 37 | run: | 38 | python -m pip install sphinx 39 | pushd docs && make html && popd 40 | if: success() && matrix.platform == 'ubuntu-latest' && matrix.lc-all == 'en_US.utf-8' && matrix.python-version == '3.9' 41 | - name: Test with pytest (Ubuntu) 42 | run: | 43 | coverage run -m pytest 44 | coverage xml 45 | - name: Uploade coverage to codecov 46 | uses: codecov/codecov-action@v3 47 | with: 48 | fail_ci_if_error: true 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/pathlib2' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.x 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel build 27 | 28 | - name: Build package 29 | run: | 30 | python -m build --sdist --wheel 31 | twine check dist/* 32 | 33 | - name: Upload packages to Jazzband 34 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 35 | uses: pypa/gh-action-pypi-publish@master 36 | with: 37 | user: jazzband 38 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 39 | repository_url: https://jazzband.co/projects/pathlib2/upload 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | /venv*/ 57 | /.idea/ 58 | /.pytest_cache/ 59 | /.mypy_cache/ 60 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: [] 2 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | History 2 | ------- 3 | 4 | Version 3.0.0 5 | ^^^^^^^^^^^^^ 6 | 7 | - Support for Python 2 has been dropped. 8 | 9 | - Sync with latest pathlib from 10 | cpython b5527688aae11d0b5af58176267a9943576e71e5 (3.11.0a5). 11 | 12 | Version 2.3.7-post1 13 | ^^^^^^^^^^^^^^^^^^^ 14 | 15 | - Drop minimum required six version (see issue #81) for the love of good old 16 | pip under Python 2, and updated code to be compatible with older six 17 | versions. Previous 2.3.7 releases were yanked to avoid potential issues. 18 | 19 | Version 2.3.7-post0 20 | ^^^^^^^^^^^^^^^^^^^ 21 | 22 | - Set minimum required six version (see issue #80). 23 | 24 | Version 2.3.7 25 | ^^^^^^^^^^^^^ 26 | 27 | - **This version will be the last release to support Python 2.7.** 28 | 29 | - Fix bug in samefile on Windows when file does not exist. 30 | 31 | - Add newline parameter for write_text (see issue #64). 32 | 33 | - Add many more type annotations. 34 | 35 | - Continuous integration migrated to github actions. 36 | 37 | - Project migrated to jazzband. 38 | 39 | Version 2.3.6 40 | ^^^^^^^^^^^^^ 41 | 42 | - Fix minor unicode bugs in with_name and with_suffix. Many thanks to 43 | ppentchev for reporting and for providing a fix. 44 | 45 | - Fix a few minor bugs. 46 | 47 | - Allow unicode file paths on systems that support it 48 | (note: unicode file paths will not work on Windows 49 | due a broken filesystem encoder on Windows on Python 2). 50 | 51 | - Remove travis and add github actions for regression testing. 52 | 53 | - Fix mypy warnings. 54 | 55 | Version 2.3.5 56 | ^^^^^^^^^^^^^ 57 | 58 | - Fall back to ascii when getfilesystemencoding returns None (see 59 | issue #59). 60 | 61 | Version 2.3.4 62 | ^^^^^^^^^^^^^ 63 | 64 | - Do not raise windows error when calling resolve on a non-existing 65 | path in Python 2.7, to match behaviour on Python 3.x (see issue #54). 66 | 67 | - Use the new collections.abc when possible (see issue #53). 68 | 69 | - Sync with upstream pathlib (see issues #47 and #51). 70 | 71 | Version 2.3.3 72 | ^^^^^^^^^^^^^ 73 | 74 | - Bring back old deprecated dependency syntax to ensure compatibility 75 | with older systems (see issue #46). 76 | 77 | - Drop Python 3.3 support, as scandir no longer supports it. 78 | 79 | - Add Python 3.7 support. 80 | 81 | Version 2.3.2 82 | ^^^^^^^^^^^^^ 83 | 84 | - Hotfix for broken setup.py. 85 | 86 | Version 2.3.1 87 | ^^^^^^^^^^^^^ 88 | 89 | - Fix tests for systems where filesystem encoding only supports ascii 90 | (reported by yurivict, fixed with help of honnibal, see issue #30). 91 | 92 | - Use modern setuptools syntax for specifying conditional scandir 93 | dependency (see issue #31). 94 | 95 | - Remove legacy use of support module from old pathlib module (see 96 | issue #39). This fixes the tests for Python 3.6. 97 | 98 | - Drop the "from __future__ import unicode_literals" and -Qnew tests 99 | as it introduced subtle bugs in the tests, and maintaining separate 100 | test modules for these legacy features seems not worth the effort. 101 | 102 | - Drop Python 3.2 support, as scandir no longer supports it. 103 | 104 | Version 2.3.0 105 | ^^^^^^^^^^^^^ 106 | 107 | - Sync with upstream pathlib from CPython 3.6.1 (7d1017d). 108 | 109 | Version 2.2.1 110 | ^^^^^^^^^^^^^ 111 | 112 | - Fix conditional scandir dependency in wheel (reported by AvdN, see 113 | issue #20 and pull request #21). 114 | 115 | Version 2.2.0 116 | ^^^^^^^^^^^^^ 117 | 118 | - Sync with upstream pathlib from CPython 3.5.2 and 3.6.0: fix various 119 | exceptions, empty glob pattern, scandir, __fspath__. 120 | 121 | - Support unicode strings to be used to construct paths in Python 2 122 | (reported by native-api, see issue #13 and pull request #15). 123 | 124 | Version 2.1.0 125 | ^^^^^^^^^^^^^ 126 | 127 | - Sync with upstream pathlib from CPython 3.5.0: gethomedir, home, 128 | expanduser. 129 | 130 | Version 2.0.1 131 | ^^^^^^^^^^^^^ 132 | 133 | - Fix TypeError exceptions in write_bytes and write_text (contributed 134 | by Emanuele Gaifas, see pull request #2). 135 | 136 | Version 2.0 137 | ^^^^^^^^^^^ 138 | 139 | - Sync with upstream pathlib from CPython: read_text, write_text, 140 | read_bytes, write_bytes, __enter__, __exit__, samefile. 141 | - Use travis and appveyor for continuous integration. 142 | - Fixed some bugs in test code. 143 | 144 | Version 1.0.1 145 | ^^^^^^^^^^^^^ 146 | 147 | - Pull request #4: Python 2.6 compatibility by eevee. 148 | 149 | Version 1.0 150 | ^^^^^^^^^^^ 151 | 152 | This version brings ``pathlib`` up to date with the official Python 3.4 153 | release, and also fixes a couple of 2.7-specific issues. 154 | 155 | - Python issue #20765: Add missing documentation for PurePath.with_name() 156 | and PurePath.with_suffix(). 157 | - Fix test_mkdir_parents when the working directory has additional bits 158 | set (such as the setgid or sticky bits). 159 | - Python issue #20111: pathlib.Path.with_suffix() now sanity checks the 160 | given suffix. 161 | - Python issue #19918: Fix PurePath.relative_to() under Windows. 162 | - Python issue #19921: When Path.mkdir() is called with parents=True, any 163 | missing parent is created with the default permissions, ignoring the mode 164 | argument (mimicking the POSIX "mkdir -p" command). 165 | - Python issue #19887: Improve the Path.resolve() algorithm to support 166 | certain symlink chains. 167 | - Make pathlib usable under Python 2.7 with unicode pathnames (only pure 168 | ASCII, though). 169 | - Issue #21: fix TypeError under Python 2.7 when using new division. 170 | - Add tox support for easier testing. 171 | 172 | Version 0.97 173 | ^^^^^^^^^^^^ 174 | 175 | This version brings ``pathlib`` up to date with the final API specified 176 | in :pep:`428`. The changes are too long to list here, it is recommended 177 | to read the `documentation `_. 178 | 179 | .. warning:: 180 | The API in this version is partially incompatible with pathlib 0.8 and 181 | earlier. Be sure to check your code for possible breakage! 182 | 183 | Version 0.8 184 | ^^^^^^^^^^^ 185 | 186 | - Add PurePath.name and PurePath.anchor. 187 | - Add Path.owner and Path.group. 188 | - Add Path.replace(). 189 | - Add Path.as_uri(). 190 | - Issue #10: when creating a file with Path.open(), don't set the executable 191 | bit. 192 | - Issue #11: fix comparisons with non-Path objects. 193 | 194 | Version 0.7 195 | ^^^^^^^^^^^ 196 | 197 | - Add '**' (recursive) patterns to Path.glob(). 198 | - Fix openat() support after the API refactoring in Python 3.3 beta1. 199 | - Add a *target_is_directory* argument to Path.symlink_to() 200 | 201 | Version 0.6 202 | ^^^^^^^^^^^ 203 | 204 | - Add Path.is_file() and Path.is_symlink() 205 | - Add Path.glob() and Path.rglob() 206 | - Add PurePath.match() 207 | 208 | Version 0.5 209 | ^^^^^^^^^^^ 210 | 211 | - Add Path.mkdir(). 212 | - Add Python 2.7 compatibility by Michele Lacchia. 213 | - Make parent() raise ValueError when the level is greater than the path 214 | length. 215 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://jazzband.co/static/img/jazzband.svg 2 | :target: https://jazzband.co/ 3 | :alt: Jazzband 4 | 5 | This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. 6 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2017 Matthias C. M. Troffaes 4 | Copyright (c) 2012-2014 Antoine Pitrou and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | recursive-include src *.py 3 | recursive-include tests *.py 4 | recursive-include docs * 5 | include *.rst 6 | include *.md 7 | include VERSION 8 | include mypy.ini 9 | include pytest.ini 10 | include .coveragerc 11 | exclude codecov.yml 12 | exclude .pre-commit-config.yaml 13 | include tests/requirements.txt 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pathlib2 2 | ======== 3 | 4 | |jazzband| |github| |codecov| 5 | 6 | Fork of pathlib aiming to support the full stdlib Python API. 7 | 8 | The `old pathlib `_ 9 | module on bitbucket is no longer maintained. 10 | The goal of pathlib2 is to provide a backport of 11 | `standard pathlib `_ 12 | module which tracks the standard library module, 13 | so all the newest features of the standard pathlib can be 14 | used also on older Python versions. 15 | 16 | Download 17 | -------- 18 | 19 | Standalone releases are available on PyPI: 20 | http://pypi.python.org/pypi/pathlib2/ 21 | 22 | Development 23 | ----------- 24 | 25 | The main development takes place in the Python standard library: see 26 | the `Python developer's guide `_. 27 | In particular, new features should be submitted to the 28 | `Python bug tracker `_. 29 | 30 | Issues that occur in this backport, but that do not occur not in the 31 | standard Python pathlib module can be submitted on 32 | the `pathlib2 bug tracker `_. 33 | 34 | Documentation 35 | ------------- 36 | 37 | Refer to the 38 | `standard pathlib `_ 39 | documentation. 40 | 41 | Known Issues 42 | ------------ 43 | 44 | For historic reasons, pathlib2 still uses bytes to represent file paths internally. 45 | Unfortunately, on Windows with Python 2.7, the file system encoder (``mcbs``) 46 | has only poor support for non-ascii characters, 47 | and can silently replace non-ascii characters without warning. 48 | For example, ``u'тест'.encode(sys.getfilesystemencoding())`` results in ``????`` 49 | which is obviously completely useless. 50 | 51 | Therefore, on Windows with Python 2.7, until this problem is fixed upstream, 52 | unfortunately you cannot rely on pathlib2 to support the full unicode range for filenames. 53 | See `issue #56 `_ for more details. 54 | 55 | .. |github| image:: https://github.com/jazzband/pathlib2/actions/workflows/python-package.yml/badge.svg 56 | :target: https://github.com/jazzband/pathlib2/actions/workflows/python-package.yml 57 | :alt: github 58 | 59 | .. |codecov| image:: https://codecov.io/gh/jazzband/pathlib2/branch/develop/graph/badge.svg 60 | :target: https://codecov.io/gh/jazzband/pathlib2 61 | :alt: codecov 62 | 63 | .. |jazzband| image:: https://jazzband.co/static/img/badge.svg 64 | :alt: Jazzband 65 | :target: https://jazzband.co/ 66 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 3.0.0a0 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | # ignore some helper code that was copied from cpython 3 | ignore: 4 | - "src/pathlib2/_ntpath.py" 5 | - "src/pathlib2/_posixpath.py" 6 | - "tests/os_helper.py" 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | project = 'pathlib2' 4 | copyright = '2012-2014 Antoine Pitrou and contributors; 2014-2021, Matthias C. M. Troffaes and contributors' 5 | author = 'Matthias C. M. Troffaes' 6 | 7 | # The full version, including alpha/beta/rc tags 8 | with open("../VERSION") as version_file: 9 | release = version_file.read().strip() 10 | 11 | # -- General configuration --------------------------------------------------- 12 | 13 | extensions = [] 14 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pathlib2's documentation! 2 | ==================================== 3 | 4 | :Release: |release| 5 | :Date: |today| 6 | 7 | .. include:: ../README.rst 8 | :start-line: 6 9 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = src/pathlib2/*.py,tests/*.py,setup.py 3 | 4 | [mypy-setuptools] 5 | ignore_missing_imports = True 6 | 7 | [mypy-nt] 8 | ignore_missing_imports = True 9 | 10 | [mypy-test.support] 11 | ignore_missing_imports = True 12 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021 Matthias C. M. Troffaes 2 | # Copyright (c) 2012-2014 Antoine Pitrou and contributors 3 | # Distributed under the terms of the MIT License. 4 | 5 | import io 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def readfile(filename): 10 | with io.open(filename, encoding="utf-8") as stream: 11 | return stream.read().split("\n") 12 | 13 | 14 | readme = readfile("README.rst")[5:] # skip title and badges 15 | version = readfile("VERSION")[0].strip() 16 | 17 | setup( 18 | name='pathlib2', 19 | version=version, 20 | packages=find_packages('src'), 21 | package_dir={'': 'src'}, 22 | license='MIT', 23 | description='Object-oriented filesystem paths', 24 | long_description="\n".join(readme[2:]), 25 | author='Matthias C. M. Troffaes', 26 | author_email='matthias.troffaes@gmail.com', 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Programming Language :: Python :: 3.10', 37 | 'Programming Language :: Python :: 3.11', 38 | 'Topic :: Software Development :: Libraries', 39 | 'Topic :: System :: Filesystems', 40 | ], 41 | url='https://github.com/jazzband/pathlib2', 42 | python_requires='>=3.8', 43 | ) 44 | -------------------------------------------------------------------------------- /src/pathlib2/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2001-2022 Python Software Foundation; All Rights Reserved 2 | # Copyright (c) 2014-2022 Matthias C. M. Troffaes and contributors 3 | # Copyright (c) 2012-2014 Antoine Pitrou and contributors 4 | # 5 | # Distributed under the terms of the MIT License. 6 | 7 | import fnmatch 8 | import functools 9 | import io 10 | import ntpath 11 | import os 12 | import posixpath 13 | import re 14 | import sys 15 | import warnings 16 | from _collections_abc import Sequence 17 | from errno import ENOENT, ENOTDIR, EBADF, ELOOP 18 | from operator import attrgetter 19 | from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO 20 | from urllib.parse import quote_from_bytes as urlquote_from_bytes 21 | 22 | 23 | __all__ = [ 24 | "PurePath", "PurePosixPath", "PureWindowsPath", 25 | "Path", "PosixPath", "WindowsPath", 26 | ] 27 | 28 | # 29 | # Internals 30 | # 31 | 32 | _WINERROR_NOT_READY = 21 # drive exists but is not accessible 33 | _WINERROR_INVALID_NAME = 123 # fix for bpo-35306 34 | _WINERROR_CANT_RESOLVE_FILENAME = 1921 # broken symlink pointing to itself 35 | 36 | # EBADF - guard against macOS `stat` throwing EBADF 37 | _IGNORED_ERRNOS = (ENOENT, ENOTDIR, EBADF, ELOOP) 38 | 39 | _IGNORED_WINERRORS = ( 40 | _WINERROR_NOT_READY, 41 | _WINERROR_INVALID_NAME, 42 | _WINERROR_CANT_RESOLVE_FILENAME) 43 | 44 | def _ignore_error(exception): 45 | return (getattr(exception, 'errno', None) in _IGNORED_ERRNOS or 46 | getattr(exception, 'winerror', None) in _IGNORED_WINERRORS) 47 | 48 | 49 | def _is_wildcard_pattern(pat): 50 | # Whether this pattern needs actual matching using fnmatch, or can 51 | # be looked up directly as a file. 52 | return "*" in pat or "?" in pat or "[" in pat 53 | 54 | 55 | if sys.version_info >= (3, 10): 56 | io_text_encoding = io.text_encoding 57 | else: 58 | def io_text_encoding(encoding, stacklevel=2): 59 | return encoding 60 | 61 | 62 | class _Flavour(object): 63 | """A flavour implements a particular (platform-specific) set of path 64 | semantics.""" 65 | 66 | sep: str 67 | altsep: str 68 | 69 | def __init__(self): 70 | self.join = self.sep.join 71 | 72 | def parse_parts(self, parts): 73 | parsed = [] 74 | sep = self.sep 75 | altsep = self.altsep 76 | drv = root = '' 77 | it = reversed(parts) 78 | for part in it: 79 | if not part: 80 | continue 81 | if altsep: 82 | part = part.replace(altsep, sep) 83 | drv, root, rel = self.splitroot(part) 84 | if sep in rel: 85 | for x in reversed(rel.split(sep)): 86 | if x and x != '.': 87 | parsed.append(sys.intern(x)) 88 | else: 89 | if rel and rel != '.': 90 | parsed.append(sys.intern(rel)) 91 | if drv or root: 92 | if not drv: 93 | # If no drive is present, try to find one in the previous 94 | # parts. This makes the result of parsing e.g. 95 | # ("C:", "/", "a") reasonably intuitive. 96 | for part in it: 97 | if not part: 98 | continue 99 | if altsep: 100 | part = part.replace(altsep, sep) 101 | drv = self.splitroot(part)[0] 102 | if drv: 103 | break 104 | break 105 | if drv or root: 106 | parsed.append(drv + root) 107 | parsed.reverse() 108 | return drv, root, parsed 109 | 110 | def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2): 111 | """ 112 | Join the two paths represented by the respective 113 | (drive, root, parts) tuples. Return a new (drive, root, parts) tuple. 114 | """ 115 | if root2: 116 | if not drv2 and drv: 117 | return drv, root2, [drv + root2] + parts2[1:] 118 | elif drv2: 119 | if drv2 == drv or self.casefold(drv2) == self.casefold(drv): 120 | # Same drive => second path is relative to the first 121 | return drv, root, parts + parts2[1:] 122 | else: 123 | # Second path is non-anchored (common case) 124 | return drv, root, parts + parts2 125 | return drv2, root2, parts2 126 | 127 | 128 | class _WindowsFlavour(_Flavour): 129 | # Reference for Windows paths can be found at 130 | # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx 131 | 132 | sep = '\\' 133 | altsep = '/' 134 | has_drv = True 135 | pathmod = ntpath 136 | 137 | is_supported = (os.name == 'nt') 138 | 139 | drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') 140 | ext_namespace_prefix = '\\\\?\\' 141 | 142 | reserved_names = ( 143 | {'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} | 144 | {'COM%s' % c for c in '123456789\xb9\xb2\xb3'} | 145 | {'LPT%s' % c for c in '123456789\xb9\xb2\xb3'} 146 | ) 147 | 148 | # Interesting findings about extended paths: 149 | # * '\\?\c:\a' is an extended path, which bypasses normal Windows API 150 | # path processing. Thus relative paths are not resolved and slash is not 151 | # translated to backslash. It has the native NT path limit of 32767 152 | # characters, but a bit less after resolving device symbolic links, 153 | # such as '\??\C:' => '\Device\HarddiskVolume2'. 154 | # * '\\?\c:/a' looks for a device named 'C:/a' because slash is a 155 | # regular name character in the object namespace. 156 | # * '\\?\c:\foo/bar' is invalid because '/' is illegal in NT filesystems. 157 | # The only path separator at the filesystem level is backslash. 158 | # * '//?/c:\a' and '//?/c:/a' are effectively equivalent to '\\.\c:\a' and 159 | # thus limited to MAX_PATH. 160 | # * Prior to Windows 8, ANSI API bytes paths are limited to MAX_PATH, 161 | # even with the '\\?\' prefix. 162 | 163 | def splitroot(self, part, sep=sep): 164 | first = part[0:1] 165 | second = part[1:2] 166 | if (second == sep and first == sep): 167 | # XXX extended paths should also disable the collapsing of "." 168 | # components (according to MSDN docs). 169 | prefix, part = self._split_extended_path(part) 170 | first = part[0:1] 171 | second = part[1:2] 172 | else: 173 | prefix = '' 174 | third = part[2:3] 175 | if (second == sep and first == sep and third != sep): 176 | # is a UNC path: 177 | # vvvvvvvvvvvvvvvvvvvvv root 178 | # \\machine\mountpoint\directory\etc\... 179 | # directory ^^^^^^^^^^^^^^ 180 | index = part.find(sep, 2) 181 | if index != -1: 182 | index2 = part.find(sep, index + 1) 183 | # a UNC path can't have two slashes in a row 184 | # (after the initial two) 185 | if index2 != index + 1: 186 | if index2 == -1: 187 | index2 = len(part) 188 | if prefix: 189 | return prefix + part[1:index2], sep, part[index2+1:] 190 | else: 191 | return part[:index2], sep, part[index2+1:] 192 | drv = root = '' 193 | if second == ':' and first in self.drive_letters: 194 | drv = part[:2] 195 | part = part[2:] 196 | first = third 197 | if first == sep: 198 | root = first 199 | part = part.lstrip(sep) 200 | return prefix + drv, root, part 201 | 202 | def casefold(self, s): 203 | return s.lower() 204 | 205 | def casefold_parts(self, parts): 206 | return [p.lower() for p in parts] 207 | 208 | def compile_pattern(self, pattern): 209 | return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch 210 | 211 | def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix): 212 | prefix = '' 213 | if s.startswith(ext_prefix): 214 | prefix = s[:4] 215 | s = s[4:] 216 | if s.startswith('UNC\\'): 217 | prefix += s[:3] 218 | s = '\\' + s[3:] 219 | return prefix, s 220 | 221 | def is_reserved(self, parts): 222 | # NOTE: the rules for reserved names seem somewhat complicated 223 | # (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not 224 | # exist). We err on the side of caution and return True for paths 225 | # which are not considered reserved by Windows. 226 | if not parts: 227 | return False 228 | if parts[0].startswith('\\\\'): 229 | # UNC paths are never reserved 230 | return False 231 | name = parts[-1].partition('.')[0].partition(':')[0].rstrip(' ') 232 | return name.upper() in self.reserved_names 233 | 234 | def make_uri(self, path): 235 | # Under Windows, file URIs use the UTF-8 encoding. 236 | drive = path.drive 237 | if len(drive) == 2 and drive[1] == ':': 238 | # It's a path on a local drive => 'file:///c:/a/b' 239 | rest = path.as_posix()[2:].lstrip('/') 240 | return 'file:///%s/%s' % ( 241 | drive, urlquote_from_bytes(rest.encode('utf-8'))) 242 | else: 243 | # It's a path on a network drive => 'file://host/share/a/b' 244 | return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) 245 | 246 | 247 | class _PosixFlavour(_Flavour): 248 | sep = '/' 249 | altsep = '' 250 | has_drv = False 251 | pathmod = posixpath 252 | 253 | is_supported = (os.name != 'nt') 254 | 255 | def splitroot(self, part, sep=sep): 256 | if part and part[0] == sep: 257 | stripped_part = part.lstrip(sep) 258 | # According to POSIX path resolution: 259 | # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 260 | # "A pathname that begins with two successive slashes may be 261 | # interpreted in an implementation-defined manner, although more 262 | # than two leading slashes shall be treated as a single slash". 263 | if len(part) - len(stripped_part) == 2: 264 | return '', sep * 2, stripped_part 265 | else: 266 | return '', sep, stripped_part 267 | else: 268 | return '', '', part 269 | 270 | def casefold(self, s): 271 | return s 272 | 273 | def casefold_parts(self, parts): 274 | return parts 275 | 276 | def compile_pattern(self, pattern): 277 | return re.compile(fnmatch.translate(pattern)).fullmatch 278 | 279 | def is_reserved(self, parts): 280 | return False 281 | 282 | def make_uri(self, path): 283 | # We represent the path using the local filesystem encoding, 284 | # for portability to other applications. 285 | bpath = bytes(path) 286 | return 'file://' + urlquote_from_bytes(bpath) 287 | 288 | 289 | _windows_flavour = _WindowsFlavour() 290 | _posix_flavour = _PosixFlavour() 291 | 292 | 293 | if sys.version_info >= (3, 10): 294 | from os.path import realpath as os_path_realpath 295 | elif os.name == "posix": 296 | from pathlib2._posixpath import realpath as os_path_realpath 297 | else: 298 | from pathlib2._ntpath import realpath as os_path_realpath 299 | 300 | 301 | # 302 | # Globbing helpers 303 | # 304 | 305 | def _make_selector(pattern_parts, flavour): 306 | pat = pattern_parts[0] 307 | child_parts = pattern_parts[1:] 308 | if pat == '**': 309 | cls = _RecursiveWildcardSelector 310 | elif '**' in pat: 311 | raise ValueError("Invalid pattern: '**' can only be an entire path component") 312 | elif _is_wildcard_pattern(pat): 313 | cls = _WildcardSelector 314 | else: 315 | cls = _PreciseSelector 316 | return cls(pat, child_parts, flavour) 317 | 318 | if hasattr(functools, "lru_cache"): 319 | _make_selector = functools.lru_cache()(_make_selector) 320 | 321 | 322 | class _Selector: 323 | """A selector matches a specific glob pattern part against the children 324 | of a given path.""" 325 | 326 | def __init__(self, child_parts, flavour): 327 | self.child_parts = child_parts 328 | if child_parts: 329 | self.successor = _make_selector(child_parts, flavour) 330 | self.dironly = True 331 | else: 332 | self.successor = _TerminatingSelector() 333 | self.dironly = False 334 | 335 | def select_from(self, parent_path): 336 | """Iterate over all child paths of `parent_path` matched by this 337 | selector. This can contain parent_path itself.""" 338 | path_cls = type(parent_path) 339 | is_dir = path_cls.is_dir 340 | exists = path_cls.exists 341 | scandir = path_cls._scandir 342 | if not is_dir(parent_path): 343 | return iter([]) 344 | return self._select_from(parent_path, is_dir, exists, scandir) 345 | 346 | 347 | class _TerminatingSelector: 348 | 349 | def _select_from(self, parent_path, is_dir, exists, scandir): 350 | yield parent_path 351 | 352 | 353 | class _PreciseSelector(_Selector): 354 | 355 | def __init__(self, name, child_parts, flavour): 356 | self.name = name 357 | _Selector.__init__(self, child_parts, flavour) 358 | 359 | def _select_from(self, parent_path, is_dir, exists, scandir): 360 | try: 361 | path = parent_path._make_child_relpath(self.name) 362 | if (is_dir if self.dironly else exists)(path): 363 | for p in self.successor._select_from(path, is_dir, exists, scandir): 364 | yield p 365 | except PermissionError: 366 | return 367 | 368 | 369 | class _WildcardSelector(_Selector): 370 | 371 | def __init__(self, pat, child_parts, flavour): 372 | self.match = flavour.compile_pattern(pat) 373 | _Selector.__init__(self, child_parts, flavour) 374 | 375 | def _select_from(self, parent_path, is_dir, exists, scandir): 376 | try: 377 | with scandir(parent_path) as scandir_it: 378 | entries = list(scandir_it) 379 | for entry in entries: 380 | if self.dironly: 381 | try: 382 | # "entry.is_dir()" can raise PermissionError 383 | # in some cases (see bpo-38894), which is not 384 | # among the errors ignored by _ignore_error() 385 | if not entry.is_dir(): 386 | continue 387 | except OSError as e: 388 | if not _ignore_error(e): 389 | raise 390 | continue 391 | name = entry.name 392 | if self.match(name): 393 | path = parent_path._make_child_relpath(name) 394 | for p in self.successor._select_from(path, is_dir, exists, scandir): 395 | yield p 396 | except PermissionError: 397 | return 398 | 399 | 400 | class _RecursiveWildcardSelector(_Selector): 401 | 402 | def __init__(self, pat, child_parts, flavour): 403 | _Selector.__init__(self, child_parts, flavour) 404 | 405 | def _iterate_directories(self, parent_path, is_dir, scandir): 406 | yield parent_path 407 | try: 408 | with scandir(parent_path) as scandir_it: 409 | entries = list(scandir_it) 410 | for entry in entries: 411 | entry_is_dir = False 412 | try: 413 | entry_is_dir = entry.is_dir() 414 | except OSError as e: 415 | if not _ignore_error(e): 416 | raise 417 | if entry_is_dir and not entry.is_symlink(): 418 | path = parent_path._make_child_relpath(entry.name) 419 | for p in self._iterate_directories(path, is_dir, scandir): 420 | yield p 421 | except PermissionError: 422 | return 423 | 424 | def _select_from(self, parent_path, is_dir, exists, scandir): 425 | try: 426 | yielded = set() 427 | try: 428 | successor_select = self.successor._select_from 429 | for starting_point in self._iterate_directories(parent_path, is_dir, scandir): 430 | for p in successor_select(starting_point, is_dir, exists, scandir): 431 | if p not in yielded: 432 | yield p 433 | yielded.add(p) 434 | finally: 435 | yielded.clear() 436 | except PermissionError: 437 | return 438 | 439 | 440 | # 441 | # Public API 442 | # 443 | 444 | class _PathParents(Sequence): 445 | """This object provides sequence-like access to the logical ancestors 446 | of a path. Don't try to construct it yourself.""" 447 | __slots__ = ('_pathcls', '_drv', '_root', '_parts') 448 | 449 | def __init__(self, path): 450 | # We don't store the instance to avoid reference cycles 451 | self._pathcls = type(path) 452 | self._drv = path._drv 453 | self._root = path._root 454 | self._parts = path._parts 455 | 456 | def __len__(self): 457 | if self._drv or self._root: 458 | return len(self._parts) - 1 459 | else: 460 | return len(self._parts) 461 | 462 | def __getitem__(self, idx): 463 | if isinstance(idx, slice): 464 | return tuple(self[i] for i in range(*idx.indices(len(self)))) 465 | 466 | if idx >= len(self) or idx < -len(self): 467 | raise IndexError(idx) 468 | return self._pathcls._from_parsed_parts(self._drv, self._root, 469 | self._parts[:-idx - 1]) 470 | 471 | def __repr__(self): 472 | return "<{}.parents>".format(self._pathcls.__name__) 473 | 474 | 475 | class PurePath(object): 476 | """Base class for manipulating paths without I/O. 477 | 478 | PurePath represents a filesystem path and offers operations which 479 | don't imply any actual filesystem I/O. Depending on your system, 480 | instantiating a PurePath will return either a PurePosixPath or a 481 | PureWindowsPath object. You can also instantiate either of these classes 482 | directly, regardless of your system. 483 | """ 484 | __slots__ = ( 485 | '_drv', '_root', '_parts', 486 | '_str', '_hash', '_pparts', '_cached_cparts', 487 | ) 488 | 489 | def __new__(cls, *args): 490 | """Construct a PurePath from one or several strings and or existing 491 | PurePath objects. The strings and path objects are combined so as 492 | to yield a canonicalized path, which is incorporated into the 493 | new PurePath object. 494 | """ 495 | if cls is PurePath: 496 | cls = PureWindowsPath if os.name == 'nt' else PurePosixPath 497 | return cls._from_parts(args) 498 | 499 | def __reduce__(self): 500 | # Using the parts tuple helps share interned path parts 501 | # when pickling related paths. 502 | return (self.__class__, tuple(self._parts)) 503 | 504 | @classmethod 505 | def _parse_args(cls, args): 506 | # This is useful when you don't want to create an instance, just 507 | # canonicalize some constructor arguments. 508 | parts = [] 509 | for a in args: 510 | if isinstance(a, PurePath): 511 | parts += a._parts 512 | else: 513 | a = os.fspath(a) 514 | if isinstance(a, str): 515 | # Force-cast str subclasses to str (issue #21127) 516 | parts.append(str(a)) 517 | else: 518 | raise TypeError( 519 | "argument should be a str object or an os.PathLike " 520 | "object returning str, not %r" 521 | % type(a)) 522 | return cls._flavour.parse_parts(parts) 523 | 524 | @classmethod 525 | def _from_parts(cls, args): 526 | # We need to call _parse_args on the instance, so as to get the 527 | # right flavour. 528 | self = object.__new__(cls) 529 | drv, root, parts = self._parse_args(args) 530 | self._drv = drv 531 | self._root = root 532 | self._parts = parts 533 | return self 534 | 535 | @classmethod 536 | def _from_parsed_parts(cls, drv, root, parts): 537 | self = object.__new__(cls) 538 | self._drv = drv 539 | self._root = root 540 | self._parts = parts 541 | return self 542 | 543 | @classmethod 544 | def _format_parsed_parts(cls, drv, root, parts): 545 | if drv or root: 546 | return drv + root + cls._flavour.join(parts[1:]) 547 | else: 548 | return cls._flavour.join(parts) 549 | 550 | def _make_child(self, args): 551 | drv, root, parts = self._parse_args(args) 552 | drv, root, parts = self._flavour.join_parsed_parts( 553 | self._drv, self._root, self._parts, drv, root, parts) 554 | return self._from_parsed_parts(drv, root, parts) 555 | 556 | def __str__(self): 557 | """Return the string representation of the path, suitable for 558 | passing to system calls.""" 559 | try: 560 | return self._str 561 | except AttributeError: 562 | self._str = self._format_parsed_parts(self._drv, self._root, 563 | self._parts) or '.' 564 | return self._str 565 | 566 | def __fspath__(self): 567 | return str(self) 568 | 569 | def as_posix(self): 570 | """Return the string representation of the path with forward (/) 571 | slashes.""" 572 | f = self._flavour 573 | return str(self).replace(f.sep, '/') 574 | 575 | def __bytes__(self): 576 | """Return the bytes representation of the path. This is only 577 | recommended to use under Unix.""" 578 | return os.fsencode(self) 579 | 580 | def __repr__(self): 581 | return "{}({!r})".format(self.__class__.__name__, self.as_posix()) 582 | 583 | def as_uri(self): 584 | """Return the path as a 'file' URI.""" 585 | if not self.is_absolute(): 586 | raise ValueError("relative path can't be expressed as a file URI") 587 | return self._flavour.make_uri(self) 588 | 589 | @property 590 | def _cparts(self): 591 | # Cached casefolded parts, for hashing and comparison 592 | try: 593 | return self._cached_cparts 594 | except AttributeError: 595 | self._cached_cparts = self._flavour.casefold_parts(self._parts) 596 | return self._cached_cparts 597 | 598 | def __eq__(self, other): 599 | if not isinstance(other, PurePath): 600 | return NotImplemented 601 | return self._cparts == other._cparts and self._flavour is other._flavour 602 | 603 | def __hash__(self): 604 | try: 605 | return self._hash 606 | except AttributeError: 607 | self._hash = hash(tuple(self._cparts)) 608 | return self._hash 609 | 610 | def __lt__(self, other): 611 | if not isinstance(other, PurePath) or self._flavour is not other._flavour: 612 | return NotImplemented 613 | return self._cparts < other._cparts 614 | 615 | def __le__(self, other): 616 | if not isinstance(other, PurePath) or self._flavour is not other._flavour: 617 | return NotImplemented 618 | return self._cparts <= other._cparts 619 | 620 | def __gt__(self, other): 621 | if not isinstance(other, PurePath) or self._flavour is not other._flavour: 622 | return NotImplemented 623 | return self._cparts > other._cparts 624 | 625 | def __ge__(self, other): 626 | if not isinstance(other, PurePath) or self._flavour is not other._flavour: 627 | return NotImplemented 628 | return self._cparts >= other._cparts 629 | 630 | drive = property(attrgetter('_drv'), 631 | doc="""The drive prefix (letter or UNC path), if any.""") 632 | 633 | root = property(attrgetter('_root'), 634 | doc="""The root of the path, if any.""") 635 | 636 | @property 637 | def anchor(self): 638 | """The concatenation of the drive and root, or ''.""" 639 | anchor = self._drv + self._root 640 | return anchor 641 | 642 | @property 643 | def name(self): 644 | """The final path component, if any.""" 645 | parts = self._parts 646 | if len(parts) == (1 if (self._drv or self._root) else 0): 647 | return '' 648 | return parts[-1] 649 | 650 | @property 651 | def suffix(self): 652 | """ 653 | The final component's last suffix, if any. 654 | 655 | This includes the leading period. For example: '.txt' 656 | """ 657 | name = self.name 658 | i = name.rfind('.') 659 | if 0 < i < len(name) - 1: 660 | return name[i:] 661 | else: 662 | return '' 663 | 664 | @property 665 | def suffixes(self): 666 | """ 667 | A list of the final component's suffixes, if any. 668 | 669 | These include the leading periods. For example: ['.tar', '.gz'] 670 | """ 671 | name = self.name 672 | if name.endswith('.'): 673 | return [] 674 | name = name.lstrip('.') 675 | return ['.' + suffix for suffix in name.split('.')[1:]] 676 | 677 | @property 678 | def stem(self): 679 | """The final path component, minus its last suffix.""" 680 | name = self.name 681 | i = name.rfind('.') 682 | if 0 < i < len(name) - 1: 683 | return name[:i] 684 | else: 685 | return name 686 | 687 | def with_name(self, name): 688 | """Return a new path with the file name changed.""" 689 | if not self.name: 690 | raise ValueError("%r has an empty name" % (self,)) 691 | drv, root, parts = self._flavour.parse_parts((name,)) 692 | if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep] 693 | or drv or root or len(parts) != 1): 694 | raise ValueError("Invalid name %r" % (name)) 695 | return self._from_parsed_parts(self._drv, self._root, 696 | self._parts[:-1] + [name]) 697 | 698 | def with_stem(self, stem): 699 | """Return a new path with the stem changed.""" 700 | return self.with_name(stem + self.suffix) 701 | 702 | def with_suffix(self, suffix): 703 | """Return a new path with the file suffix changed. If the path 704 | has no suffix, add given suffix. If the given suffix is an empty 705 | string, remove the suffix from the path. 706 | """ 707 | f = self._flavour 708 | if f.sep in suffix or f.altsep and f.altsep in suffix: 709 | raise ValueError("Invalid suffix %r" % (suffix,)) 710 | if suffix and not suffix.startswith('.') or suffix == '.': 711 | raise ValueError("Invalid suffix %r" % (suffix)) 712 | name = self.name 713 | if not name: 714 | raise ValueError("%r has an empty name" % (self,)) 715 | old_suffix = self.suffix 716 | if not old_suffix: 717 | name = name + suffix 718 | else: 719 | name = name[:-len(old_suffix)] + suffix 720 | return self._from_parsed_parts(self._drv, self._root, 721 | self._parts[:-1] + [name]) 722 | 723 | def relative_to(self, *other): 724 | """Return the relative path to another path identified by the passed 725 | arguments. If the operation is not possible (because this is not 726 | a subpath of the other path), raise ValueError. 727 | """ 728 | # For the purpose of this method, drive and root are considered 729 | # separate parts, i.e.: 730 | # Path('c:/').relative_to('c:') gives Path('/') 731 | # Path('c:/').relative_to('/') raise ValueError 732 | if not other: 733 | raise TypeError("need at least one argument") 734 | parts = self._parts 735 | drv = self._drv 736 | root = self._root 737 | if root: 738 | abs_parts = [drv, root] + parts[1:] 739 | else: 740 | abs_parts = parts 741 | to_drv, to_root, to_parts = self._parse_args(other) 742 | if to_root: 743 | to_abs_parts = [to_drv, to_root] + to_parts[1:] 744 | else: 745 | to_abs_parts = to_parts 746 | n = len(to_abs_parts) 747 | cf = self._flavour.casefold_parts 748 | if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): 749 | formatted = self._format_parsed_parts(to_drv, to_root, to_parts) 750 | raise ValueError("{!r} is not in the subpath of {!r}" 751 | " OR one path is relative and the other is absolute." 752 | .format(str(self), str(formatted))) 753 | return self._from_parsed_parts('', root if n == 1 else '', 754 | abs_parts[n:]) 755 | 756 | def is_relative_to(self, *other): 757 | """Return True if the path is relative to another path or False. 758 | """ 759 | try: 760 | self.relative_to(*other) 761 | return True 762 | except ValueError: 763 | return False 764 | 765 | @property 766 | def parts(self): 767 | """An object providing sequence-like access to the 768 | components in the filesystem path.""" 769 | # We cache the tuple to avoid building a new one each time .parts 770 | # is accessed. XXX is this necessary? 771 | try: 772 | return self._pparts 773 | except AttributeError: 774 | self._pparts = tuple(self._parts) 775 | return self._pparts 776 | 777 | def joinpath(self, *args): 778 | """Combine this path with one or several arguments, and return a 779 | new path representing either a subpath (if all arguments are relative 780 | paths) or a totally different path (if one of the arguments is 781 | anchored). 782 | """ 783 | return self._make_child(args) 784 | 785 | def __truediv__(self, key): 786 | try: 787 | return self._make_child((key,)) 788 | except TypeError: 789 | return NotImplemented 790 | 791 | def __rtruediv__(self, key): 792 | try: 793 | return self._from_parts([key] + self._parts) 794 | except TypeError: 795 | return NotImplemented 796 | 797 | @property 798 | def parent(self): 799 | """The logical parent of the path.""" 800 | drv = self._drv 801 | root = self._root 802 | parts = self._parts 803 | if len(parts) == 1 and (drv or root): 804 | return self 805 | return self._from_parsed_parts(drv, root, parts[:-1]) 806 | 807 | @property 808 | def parents(self): 809 | """A sequence of this path's logical parents.""" 810 | return _PathParents(self) 811 | 812 | def is_absolute(self): 813 | """True if the path is absolute (has both a root and, if applicable, 814 | a drive).""" 815 | if not self._root: 816 | return False 817 | return not self._flavour.has_drv or bool(self._drv) 818 | 819 | def is_reserved(self): 820 | """Return True if the path contains one of the special names reserved 821 | by the system, if any.""" 822 | return self._flavour.is_reserved(self._parts) 823 | 824 | def match(self, path_pattern): 825 | """ 826 | Return True if this path matches the given pattern. 827 | """ 828 | cf = self._flavour.casefold 829 | path_pattern = cf(path_pattern) 830 | drv, root, pat_parts = self._flavour.parse_parts((path_pattern,)) 831 | if not pat_parts: 832 | raise ValueError("empty pattern") 833 | if drv and drv != cf(self._drv): 834 | return False 835 | if root and root != cf(self._root): 836 | return False 837 | parts = self._cparts 838 | if drv or root: 839 | if len(pat_parts) != len(parts): 840 | return False 841 | pat_parts = pat_parts[1:] 842 | elif len(pat_parts) > len(parts): 843 | return False 844 | for part, pat in zip(reversed(parts), reversed(pat_parts)): 845 | if not fnmatch.fnmatchcase(part, pat): 846 | return False 847 | return True 848 | 849 | # Can't subclass os.PathLike from PurePath and keep the constructor 850 | # optimizations in PurePath._parse_args(). 851 | os.PathLike.register(PurePath) 852 | 853 | 854 | class PurePosixPath(PurePath): 855 | """PurePath subclass for non-Windows systems. 856 | 857 | On a POSIX system, instantiating a PurePath should return this object. 858 | However, you can also instantiate it directly on any system. 859 | """ 860 | _flavour = _posix_flavour 861 | __slots__ = () 862 | 863 | 864 | class PureWindowsPath(PurePath): 865 | """PurePath subclass for Windows systems. 866 | 867 | On a Windows system, instantiating a PurePath should return this object. 868 | However, you can also instantiate it directly on any system. 869 | """ 870 | _flavour = _windows_flavour 871 | __slots__ = () 872 | 873 | 874 | # Filesystem-accessing classes 875 | 876 | 877 | class Path(PurePath): 878 | """PurePath subclass that can make system calls. 879 | 880 | Path represents a filesystem path but unlike PurePath, also offers 881 | methods to do system calls on path objects. Depending on your system, 882 | instantiating a Path will return either a PosixPath or a WindowsPath 883 | object. You can also instantiate a PosixPath or WindowsPath directly, 884 | but cannot instantiate a WindowsPath on a POSIX system or vice versa. 885 | """ 886 | __slots__ = () 887 | 888 | def __new__(cls, *args, **kwargs): 889 | if cls is Path: 890 | cls = WindowsPath if os.name == 'nt' else PosixPath 891 | self = cls._from_parts(args) 892 | if not self._flavour.is_supported: 893 | raise NotImplementedError("cannot instantiate %r on your system" 894 | % (cls.__name__,)) 895 | return self 896 | 897 | def _make_child_relpath(self, part): 898 | # This is an optimization used for dir walking. `part` must be 899 | # a single part relative to this path. 900 | parts = self._parts + [part] 901 | return self._from_parsed_parts(self._drv, self._root, parts) 902 | 903 | def __enter__(self): 904 | return self 905 | 906 | def __exit__(self, t, v, tb): 907 | # https://bugs.python.org/issue39682 908 | # In previous versions of pathlib, this method marked this path as 909 | # closed; subsequent attempts to perform I/O would raise an IOError. 910 | # This functionality was never documented, and had the effect of 911 | # making Path objects mutable, contrary to PEP 428. In Python 3.9 the 912 | # _closed attribute was removed, and this method made a no-op. 913 | # This method and __enter__()/__exit__() should be deprecated and 914 | # removed in the future. 915 | pass 916 | 917 | # Public API 918 | 919 | @classmethod 920 | def cwd(cls): 921 | """Return a new path pointing to the current working directory 922 | (as returned by os.getcwd()). 923 | """ 924 | return cls(os.getcwd()) 925 | 926 | @classmethod 927 | def home(cls): 928 | """Return a new path pointing to the user's home directory (as 929 | returned by os.path.expanduser('~')). 930 | """ 931 | return cls("~").expanduser() 932 | 933 | def samefile(self, other_path): 934 | """Return whether other_path is the same or not as this file 935 | (as returned by os.path.samefile()). 936 | """ 937 | st = self.stat() 938 | try: 939 | other_st = other_path.stat() 940 | except AttributeError: 941 | other_st = self.__class__(other_path).stat() 942 | return os.path.samestat(st, other_st) 943 | 944 | def iterdir(self): 945 | """Iterate over the files in this directory. Does not yield any 946 | result for the special paths '.' and '..'. 947 | """ 948 | for name in os.listdir(self): 949 | yield self._make_child_relpath(name) 950 | 951 | def _scandir(self): 952 | # bpo-24132: a future version of pathlib will support subclassing of 953 | # pathlib.Path to customize how the filesystem is accessed. This 954 | # includes scandir(), which is used to implement glob(). 955 | return os.scandir(self) 956 | 957 | def glob(self, pattern): 958 | """Iterate over this subtree and yield all existing files (of any 959 | kind, including directories) matching the given relative pattern. 960 | """ 961 | sys.audit("pathlib.Path.glob", self, pattern) 962 | if not pattern: 963 | raise ValueError("Unacceptable pattern: {!r}".format(pattern)) 964 | drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) 965 | if drv or root: 966 | raise NotImplementedError("Non-relative patterns are unsupported") 967 | selector = _make_selector(tuple(pattern_parts), self._flavour) 968 | for p in selector.select_from(self): 969 | yield p 970 | 971 | def rglob(self, pattern): 972 | """Recursively yield all existing files (of any kind, including 973 | directories) matching the given relative pattern, anywhere in 974 | this subtree. 975 | """ 976 | sys.audit("pathlib.Path.rglob", self, pattern) 977 | drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) 978 | if drv or root: 979 | raise NotImplementedError("Non-relative patterns are unsupported") 980 | selector = _make_selector(("**",) + tuple(pattern_parts), self._flavour) 981 | for p in selector.select_from(self): 982 | yield p 983 | 984 | def absolute(self): 985 | """Return an absolute version of this path by prepending the current 986 | working directory. No normalization or symlink resolution is performed. 987 | 988 | Use resolve() to get the canonical path to a file. 989 | """ 990 | if self.is_absolute(): 991 | return self 992 | return self._from_parts([self.cwd()] + self._parts) 993 | 994 | def resolve(self, strict=False): 995 | """ 996 | Make the path absolute, resolving all symlinks on the way and also 997 | normalizing it. 998 | """ 999 | 1000 | def check_eloop(e): 1001 | winerror = getattr(e, 'winerror', 0) 1002 | if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME: 1003 | raise RuntimeError("Symlink loop from %r" % e.filename) 1004 | 1005 | try: 1006 | s = os_path_realpath(self, strict=strict) 1007 | except OSError as e: 1008 | check_eloop(e) 1009 | raise 1010 | p = self._from_parts((s,)) 1011 | 1012 | # In non-strict mode, realpath() doesn't raise on symlink loops. 1013 | # Ensure we get an exception by calling stat() 1014 | if not strict: 1015 | try: 1016 | p.stat() 1017 | except OSError as e: 1018 | check_eloop(e) 1019 | return p 1020 | 1021 | def stat(self, *, follow_symlinks=True): 1022 | """ 1023 | Return the result of the stat() system call on this path, like 1024 | os.stat() does. 1025 | """ 1026 | return os.stat(self, follow_symlinks=follow_symlinks) 1027 | 1028 | def owner(self): 1029 | """ 1030 | Return the login name of the file owner. 1031 | """ 1032 | try: 1033 | import pwd 1034 | return pwd.getpwuid(self.stat().st_uid).pw_name 1035 | except ImportError: 1036 | raise NotImplementedError("Path.owner() is unsupported on this system") 1037 | 1038 | def group(self): 1039 | """ 1040 | Return the group name of the file gid. 1041 | """ 1042 | 1043 | try: 1044 | import grp 1045 | return grp.getgrgid(self.stat().st_gid).gr_name 1046 | except ImportError: 1047 | raise NotImplementedError("Path.group() is unsupported on this system") 1048 | 1049 | def open(self, mode='r', buffering=-1, encoding=None, 1050 | errors=None, newline=None): 1051 | """ 1052 | Open the file pointed by this path and return a file object, as 1053 | the built-in open() function does. 1054 | """ 1055 | if "b" not in mode: 1056 | encoding = io_text_encoding(encoding) 1057 | return io.open(self, mode, buffering, encoding, errors, newline) 1058 | 1059 | def read_bytes(self): 1060 | """ 1061 | Open the file in bytes mode, read it, and close the file. 1062 | """ 1063 | with self.open(mode='rb') as f: 1064 | return f.read() 1065 | 1066 | def read_text(self, encoding=None, errors=None): 1067 | """ 1068 | Open the file in text mode, read it, and close the file. 1069 | """ 1070 | encoding = io_text_encoding(encoding) 1071 | with self.open(mode='r', encoding=encoding, errors=errors) as f: 1072 | return f.read() 1073 | 1074 | def write_bytes(self, data): 1075 | """ 1076 | Open the file in bytes mode, write to it, and close the file. 1077 | """ 1078 | # type-check for the buffer interface before truncating the file 1079 | view = memoryview(data) 1080 | with self.open(mode='wb') as f: 1081 | return f.write(view) 1082 | 1083 | def write_text(self, data, encoding=None, errors=None, newline=None): 1084 | """ 1085 | Open the file in text mode, write to it, and close the file. 1086 | """ 1087 | if not isinstance(data, str): 1088 | raise TypeError('data must be str, not %s' % 1089 | data.__class__.__name__) 1090 | encoding = io_text_encoding(encoding) 1091 | with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f: 1092 | return f.write(data) 1093 | 1094 | def readlink(self): 1095 | """ 1096 | Return the path to which the symbolic link points. 1097 | """ 1098 | if not hasattr(os, "readlink"): 1099 | raise NotImplementedError("os.readlink() not available on this system") 1100 | return self._from_parts((os.readlink(self),)) 1101 | 1102 | def touch(self, mode=0o666, exist_ok=True): 1103 | """ 1104 | Create this file with the given access mode, if it doesn't exist. 1105 | """ 1106 | 1107 | if exist_ok: 1108 | # First try to bump modification time 1109 | # Implementation note: GNU touch uses the UTIME_NOW option of 1110 | # the utimensat() / futimens() functions. 1111 | try: 1112 | os.utime(self, None) 1113 | except OSError: 1114 | # Avoid exception chaining 1115 | pass 1116 | else: 1117 | return 1118 | flags = os.O_CREAT | os.O_WRONLY 1119 | if not exist_ok: 1120 | flags |= os.O_EXCL 1121 | fd = os.open(self, flags, mode) 1122 | os.close(fd) 1123 | 1124 | def mkdir(self, mode=0o777, parents=False, exist_ok=False): 1125 | """ 1126 | Create a new directory at this given path. 1127 | """ 1128 | try: 1129 | os.mkdir(self, mode) 1130 | except FileNotFoundError: 1131 | if not parents or self.parent == self: 1132 | raise 1133 | self.parent.mkdir(parents=True, exist_ok=True) 1134 | self.mkdir(mode, parents=False, exist_ok=exist_ok) 1135 | except OSError: 1136 | # Cannot rely on checking for EEXIST, since the operating system 1137 | # could give priority to other errors like EACCES or EROFS 1138 | if not exist_ok or not self.is_dir(): 1139 | raise 1140 | 1141 | def chmod(self, mode, *, follow_symlinks=True): 1142 | """ 1143 | Change the permissions of the path, like os.chmod(). 1144 | """ 1145 | os.chmod(self, mode, follow_symlinks=follow_symlinks) 1146 | 1147 | def lchmod(self, mode): 1148 | """ 1149 | Like chmod(), except if the path points to a symlink, the symlink's 1150 | permissions are changed, rather than its target's. 1151 | """ 1152 | self.chmod(mode, follow_symlinks=False) 1153 | 1154 | def unlink(self, missing_ok=False): 1155 | """ 1156 | Remove this file or link. 1157 | If the path is a directory, use rmdir() instead. 1158 | """ 1159 | try: 1160 | os.unlink(self) 1161 | except FileNotFoundError: 1162 | if not missing_ok: 1163 | raise 1164 | 1165 | def rmdir(self): 1166 | """ 1167 | Remove this directory. The directory must be empty. 1168 | """ 1169 | os.rmdir(self) 1170 | 1171 | def lstat(self): 1172 | """ 1173 | Like stat(), except if the path points to a symlink, the symlink's 1174 | status information is returned, rather than its target's. 1175 | """ 1176 | return self.stat(follow_symlinks=False) 1177 | 1178 | def rename(self, target): 1179 | """ 1180 | Rename this path to the target path. 1181 | 1182 | The target path may be absolute or relative. Relative paths are 1183 | interpreted relative to the current working directory, *not* the 1184 | directory of the Path object. 1185 | 1186 | Returns the new Path instance pointing to the target path. 1187 | """ 1188 | os.rename(self, target) 1189 | return self.__class__(target) 1190 | 1191 | def replace(self, target): 1192 | """ 1193 | Rename this path to the target path, overwriting if that path exists. 1194 | 1195 | The target path may be absolute or relative. Relative paths are 1196 | interpreted relative to the current working directory, *not* the 1197 | directory of the Path object. 1198 | 1199 | Returns the new Path instance pointing to the target path. 1200 | """ 1201 | os.replace(self, target) 1202 | return self.__class__(target) 1203 | 1204 | def symlink_to(self, target, target_is_directory=False): 1205 | """ 1206 | Make this path a symlink pointing to the target path. 1207 | Note the order of arguments (link, target) is the reverse of os.symlink. 1208 | """ 1209 | if not hasattr(os, "symlink"): 1210 | raise NotImplementedError("os.symlink() not available on this system") 1211 | os.symlink(target, self, target_is_directory) 1212 | 1213 | def hardlink_to(self, target): 1214 | """ 1215 | Make this path a hard link pointing to the same file as *target*. 1216 | 1217 | Note the order of arguments (self, target) is the reverse of os.link's. 1218 | """ 1219 | if not hasattr(os, "link"): 1220 | raise NotImplementedError("os.link() not available on this system") 1221 | os.link(target, self) 1222 | 1223 | def link_to(self, target): 1224 | """ 1225 | Make the target path a hard link pointing to this path. 1226 | 1227 | Note this function does not make this path a hard link to *target*, 1228 | despite the implication of the function and argument names. The order 1229 | of arguments (target, link) is the reverse of Path.symlink_to, but 1230 | matches that of os.link. 1231 | 1232 | Deprecated since Python 3.10 and scheduled for removal in Python 3.12. 1233 | Use `hardlink_to()` instead. 1234 | """ 1235 | warnings.warn("pathlib.Path.link_to() is deprecated and is scheduled " 1236 | "for removal in Python 3.12. " 1237 | "Use pathlib.Path.hardlink_to() instead.", 1238 | DeprecationWarning, stacklevel=2) 1239 | self.__class__(target).hardlink_to(self) 1240 | 1241 | # Convenience functions for querying the stat results 1242 | 1243 | def exists(self): 1244 | """ 1245 | Whether this path exists. 1246 | """ 1247 | try: 1248 | self.stat() 1249 | except OSError as e: 1250 | if not _ignore_error(e): 1251 | raise 1252 | return False 1253 | except ValueError: 1254 | # Non-encodable path 1255 | return False 1256 | return True 1257 | 1258 | def is_dir(self): 1259 | """ 1260 | Whether this path is a directory. 1261 | """ 1262 | try: 1263 | return S_ISDIR(self.stat().st_mode) 1264 | except OSError as e: 1265 | if not _ignore_error(e): 1266 | raise 1267 | # Path doesn't exist or is a broken symlink 1268 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1269 | return False 1270 | except ValueError: 1271 | # Non-encodable path 1272 | return False 1273 | 1274 | def is_file(self): 1275 | """ 1276 | Whether this path is a regular file (also True for symlinks pointing 1277 | to regular files). 1278 | """ 1279 | try: 1280 | return S_ISREG(self.stat().st_mode) 1281 | except OSError as e: 1282 | if not _ignore_error(e): 1283 | raise 1284 | # Path doesn't exist or is a broken symlink 1285 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1286 | return False 1287 | except ValueError: 1288 | # Non-encodable path 1289 | return False 1290 | 1291 | def is_mount(self): 1292 | """ 1293 | Check if this path is a POSIX mount point 1294 | """ 1295 | # Need to exist and be a dir 1296 | if not self.exists() or not self.is_dir(): 1297 | return False 1298 | 1299 | try: 1300 | parent_dev = self.parent.stat().st_dev 1301 | except OSError: 1302 | return False 1303 | 1304 | dev = self.stat().st_dev 1305 | if dev != parent_dev: 1306 | return True 1307 | ino = self.stat().st_ino 1308 | parent_ino = self.parent.stat().st_ino 1309 | return ino == parent_ino 1310 | 1311 | def is_symlink(self): 1312 | """ 1313 | Whether this path is a symbolic link. 1314 | """ 1315 | try: 1316 | return S_ISLNK(self.lstat().st_mode) 1317 | except OSError as e: 1318 | if not _ignore_error(e): 1319 | raise 1320 | # Path doesn't exist 1321 | return False 1322 | except ValueError: 1323 | # Non-encodable path 1324 | return False 1325 | 1326 | def is_block_device(self): 1327 | """ 1328 | Whether this path is a block device. 1329 | """ 1330 | try: 1331 | return S_ISBLK(self.stat().st_mode) 1332 | except OSError as e: 1333 | if not _ignore_error(e): 1334 | raise 1335 | # Path doesn't exist or is a broken symlink 1336 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1337 | return False 1338 | except ValueError: 1339 | # Non-encodable path 1340 | return False 1341 | 1342 | def is_char_device(self): 1343 | """ 1344 | Whether this path is a character device. 1345 | """ 1346 | try: 1347 | return S_ISCHR(self.stat().st_mode) 1348 | except OSError as e: 1349 | if not _ignore_error(e): 1350 | raise 1351 | # Path doesn't exist or is a broken symlink 1352 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1353 | return False 1354 | except ValueError: 1355 | # Non-encodable path 1356 | return False 1357 | 1358 | def is_fifo(self): 1359 | """ 1360 | Whether this path is a FIFO. 1361 | """ 1362 | try: 1363 | return S_ISFIFO(self.stat().st_mode) 1364 | except OSError as e: 1365 | if not _ignore_error(e): 1366 | raise 1367 | # Path doesn't exist or is a broken symlink 1368 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1369 | return False 1370 | except ValueError: 1371 | # Non-encodable path 1372 | return False 1373 | 1374 | def is_socket(self): 1375 | """ 1376 | Whether this path is a socket. 1377 | """ 1378 | try: 1379 | return S_ISSOCK(self.stat().st_mode) 1380 | except OSError as e: 1381 | if not _ignore_error(e): 1382 | raise 1383 | # Path doesn't exist or is a broken symlink 1384 | # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ ) 1385 | return False 1386 | except ValueError: 1387 | # Non-encodable path 1388 | return False 1389 | 1390 | def expanduser(self): 1391 | """ Return a new path with expanded ~ and ~user constructs 1392 | (as returned by os.path.expanduser) 1393 | """ 1394 | if (not (self._drv or self._root) and 1395 | self._parts and self._parts[0][:1] == '~'): 1396 | homedir = os.path.expanduser(self._parts[0]) 1397 | if homedir[:1] == "~": 1398 | raise RuntimeError("Could not determine home directory.") 1399 | return self._from_parts([homedir] + self._parts[1:]) 1400 | 1401 | return self 1402 | 1403 | 1404 | class PosixPath(Path, PurePosixPath): 1405 | """Path subclass for non-Windows systems. 1406 | 1407 | On a POSIX system, instantiating a Path should return this object. 1408 | """ 1409 | __slots__ = () 1410 | 1411 | class WindowsPath(Path, PureWindowsPath): 1412 | """Path subclass for Windows systems. 1413 | 1414 | On a Windows system, instantiating a Path should return this object. 1415 | """ 1416 | __slots__ = () 1417 | 1418 | def is_mount(self): 1419 | raise NotImplementedError("Path.is_mount() is unsupported on this system") -------------------------------------------------------------------------------- /src/pathlib2/_ntpath.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import abspath, normcase, isabs, islink, normpath, join, dirname, split, devnull 3 | 4 | try: 5 | from nt import _getfinalpathname, readlink as _nt_readlink 6 | except ImportError: 7 | # realpath is a no-op on systems without _getfinalpathname support. 8 | def realpath(path, *, strict=False): 9 | return abspath(path) 10 | else: 11 | def _readlink_deep(path): 12 | # These error codes indicate that we should stop reading links and 13 | # return the path we currently have. 14 | # 1: ERROR_INVALID_FUNCTION 15 | # 2: ERROR_FILE_NOT_FOUND 16 | # 3: ERROR_DIRECTORY_NOT_FOUND 17 | # 5: ERROR_ACCESS_DENIED 18 | # 21: ERROR_NOT_READY (implies drive with no media) 19 | # 32: ERROR_SHARING_VIOLATION (probably an NTFS paging file) 20 | # 50: ERROR_NOT_SUPPORTED (implies no support for reparse points) 21 | # 67: ERROR_BAD_NET_NAME (implies remote server unavailable) 22 | # 87: ERROR_INVALID_PARAMETER 23 | # 4390: ERROR_NOT_A_REPARSE_POINT 24 | # 4392: ERROR_INVALID_REPARSE_DATA 25 | # 4393: ERROR_REPARSE_TAG_INVALID 26 | allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 67, 87, 4390, 4392, 4393 27 | 28 | seen = set() 29 | while normcase(path) not in seen: 30 | seen.add(normcase(path)) 31 | try: 32 | old_path = path 33 | path = _nt_readlink(path) 34 | # Links may be relative, so resolve them against their 35 | # own location 36 | if not isabs(path): 37 | # If it's something other than a symlink, we don't know 38 | # what it's actually going to be resolved against, so 39 | # just return the old path. 40 | if not islink(old_path): 41 | path = old_path 42 | break 43 | path = normpath(join(dirname(old_path), path)) 44 | except OSError as ex: 45 | if ex.winerror in allowed_winerror: 46 | break 47 | raise 48 | except ValueError: 49 | # Stop on reparse points that are not symlinks 50 | break 51 | return path 52 | 53 | def _getfinalpathname_nonstrict(path): 54 | # These error codes indicate that we should stop resolving the path 55 | # and return the value we currently have. 56 | # 1: ERROR_INVALID_FUNCTION 57 | # 2: ERROR_FILE_NOT_FOUND 58 | # 3: ERROR_DIRECTORY_NOT_FOUND 59 | # 5: ERROR_ACCESS_DENIED 60 | # 21: ERROR_NOT_READY (implies drive with no media) 61 | # 32: ERROR_SHARING_VIOLATION (probably an NTFS paging file) 62 | # 50: ERROR_NOT_SUPPORTED 63 | # 67: ERROR_BAD_NET_NAME (implies remote server unavailable) 64 | # 87: ERROR_INVALID_PARAMETER 65 | # 123: ERROR_INVALID_NAME 66 | # 1920: ERROR_CANT_ACCESS_FILE 67 | # 1921: ERROR_CANT_RESOLVE_FILENAME (implies unfollowable symlink) 68 | allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 67, 87, 123, 1920, 1921 69 | 70 | # Non-strict algorithm is to find as much of the target directory 71 | # as we can and join the rest. 72 | tail = '' 73 | while path: 74 | try: 75 | path = _getfinalpathname(path) 76 | return join(path, tail) if tail else path 77 | except OSError as ex: 78 | if ex.winerror not in allowed_winerror: 79 | raise 80 | try: 81 | # The OS could not resolve this path fully, so we attempt 82 | # to follow the link ourselves. If we succeed, join the tail 83 | # and return. 84 | new_path = _readlink_deep(path) 85 | if new_path != path: 86 | return join(new_path, tail) if tail else new_path 87 | except OSError: 88 | # If we fail to readlink(), let's keep traversing 89 | pass 90 | path, name = split(path) 91 | # TODO (bpo-38186): Request the real file name from the directory 92 | # entry using FindFirstFileW. For now, we will return the path 93 | # as best we have it 94 | if path and not name: 95 | return path + tail 96 | tail = join(name, tail) if tail else name 97 | return tail 98 | 99 | def realpath(path, *, strict=False): 100 | path = normpath(path) 101 | if isinstance(path, bytes): 102 | prefix = b'\\\\?\\' 103 | unc_prefix = b'\\\\?\\UNC\\' 104 | new_unc_prefix = b'\\\\' 105 | cwd = os.getcwdb() 106 | # bpo-38081: Special case for realpath(b'nul') 107 | if normcase(path) == normcase(os.fsencode(devnull)): 108 | return b'\\\\.\\NUL' 109 | else: 110 | prefix = '\\\\?\\' 111 | unc_prefix = '\\\\?\\UNC\\' 112 | new_unc_prefix = '\\\\' 113 | cwd = os.getcwd() 114 | # bpo-38081: Special case for realpath('nul') 115 | if normcase(path) == normcase(devnull): 116 | return '\\\\.\\NUL' 117 | had_prefix = path.startswith(prefix) 118 | if not had_prefix and not isabs(path): 119 | path = join(cwd, path) 120 | try: 121 | path = _getfinalpathname(path) 122 | initial_winerror = 0 123 | except OSError as ex: 124 | if strict: 125 | raise 126 | initial_winerror = ex.winerror 127 | path = _getfinalpathname_nonstrict(path) 128 | # The path returned by _getfinalpathname will always start with \\?\ - 129 | # strip off that prefix unless it was already provided on the original 130 | # path. 131 | if not had_prefix and path.startswith(prefix): 132 | # For UNC paths, the prefix will actually be \\?\UNC\ 133 | # Handle that case as well. 134 | if path.startswith(unc_prefix): 135 | spath = new_unc_prefix + path[len(unc_prefix):] 136 | else: 137 | spath = path[len(prefix):] 138 | # Ensure that the non-prefixed path resolves to the same path 139 | try: 140 | if _getfinalpathname(spath) == path: 141 | path = spath 142 | except OSError as ex: 143 | # If the path does not exist and originally did not exist, then 144 | # strip the prefix anyway. 145 | if ex.winerror == initial_winerror: 146 | path = spath 147 | return path 148 | -------------------------------------------------------------------------------- /src/pathlib2/_posixpath.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | from os.path import abspath, split, join, isabs 4 | 5 | 6 | def realpath(filename, *, strict=False): 7 | """Return the canonical path of the specified filename, eliminating any 8 | symbolic links encountered in the path.""" 9 | filename = os.fspath(filename) 10 | path, ok = _joinrealpath(filename[:0], filename, strict, {}) 11 | return abspath(path) 12 | 13 | 14 | # Join two paths, normalizing and eliminating any symbolic links 15 | # encountered in the second path. 16 | def _joinrealpath(path, rest, strict, seen): 17 | if isinstance(path, bytes): 18 | sep = b'/' 19 | curdir = b'.' 20 | pardir = b'..' 21 | else: 22 | sep = '/' 23 | curdir = '.' 24 | pardir = '..' 25 | 26 | if isabs(rest): 27 | rest = rest[1:] 28 | path = sep 29 | 30 | while rest: 31 | name, _, rest = rest.partition(sep) 32 | if not name or name == curdir: 33 | # current dir 34 | continue 35 | if name == pardir: 36 | # parent dir 37 | if path: 38 | path, name = split(path) 39 | if name == pardir: 40 | path = join(path, pardir, pardir) 41 | else: 42 | path = pardir 43 | continue 44 | newpath = join(path, name) 45 | try: 46 | st = os.lstat(newpath) 47 | except OSError: 48 | if strict: 49 | raise 50 | is_link = False 51 | else: 52 | is_link = stat.S_ISLNK(st.st_mode) 53 | if not is_link: 54 | path = newpath 55 | continue 56 | # Resolve the symbolic link 57 | if newpath in seen: 58 | # Already seen this path 59 | path = seen[newpath] 60 | if path is not None: 61 | # use cached value 62 | continue 63 | # The symlink is not resolved, so we must have a symlink loop. 64 | if strict: 65 | # Raise OSError(errno.ELOOP) 66 | os.stat(newpath) 67 | else: 68 | # Return already resolved part + rest of the path unchanged. 69 | return join(newpath, rest), False 70 | seen[newpath] = None # not resolved symlink 71 | path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen) 72 | if not ok: 73 | return join(path, rest), False 74 | seen[newpath] = path # resolved symlink 75 | 76 | return path, True 77 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/pathlib2/0ac11b8d697069abea58a8188061f1a1870977af/tests/__init__.py -------------------------------------------------------------------------------- /tests/os_helper.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import contextlib 3 | import errno 4 | import os 5 | import re 6 | import stat 7 | import sys 8 | import time 9 | import unittest 10 | import warnings 11 | 12 | 13 | # Filename used for testing 14 | from typing import Optional 15 | 16 | if os.name == 'java': 17 | # Jython disallows @ in module names 18 | TESTFN_ASCII = '$test' 19 | else: 20 | TESTFN_ASCII = '@test' 21 | 22 | # Disambiguate TESTFN for parallel testing, while letting it remain a valid 23 | # module name. 24 | TESTFN_ASCII = f"{TESTFN_ASCII}_{os.getpid()}_tmp" 25 | 26 | # TESTFN_UNICODE is a non-ascii filename 27 | TESTFN_UNICODE = TESTFN_ASCII + "-\xe0\xf2\u0258\u0141\u011f" 28 | if sys.platform == 'darwin': 29 | # In Mac OS X's VFS API file names are, by definition, canonically 30 | # decomposed Unicode, encoded using UTF-8. See QA1173: 31 | # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html 32 | import unicodedata 33 | TESTFN_UNICODE = unicodedata.normalize('NFD', TESTFN_UNICODE) 34 | 35 | # TESTFN_UNENCODABLE is a filename (str type) that should *not* be able to be 36 | # encoded by the filesystem encoding (in strict mode). It can be None if we 37 | # cannot generate such filename. 38 | TESTFN_UNENCODABLE = None 39 | if os.name == 'nt': 40 | # skip win32s (0) or Windows 9x/ME (1) 41 | if sys.getwindowsversion().platform >= 2: 42 | # Different kinds of characters from various languages to minimize the 43 | # probability that the whole name is encodable to MBCS (issue #9819) 44 | TESTFN_UNENCODABLE = TESTFN_ASCII + "-\u5171\u0141\u2661\u0363\uDC80" 45 | try: 46 | TESTFN_UNENCODABLE.encode(sys.getfilesystemencoding()) 47 | except UnicodeEncodeError: 48 | pass 49 | else: 50 | print('WARNING: The filename %r CAN be encoded by the filesystem ' 51 | 'encoding (%s). Unicode filename tests may not be effective' 52 | % (TESTFN_UNENCODABLE, sys.getfilesystemencoding())) 53 | TESTFN_UNENCODABLE = None 54 | # Mac OS X denies unencodable filenames (invalid utf-8) 55 | elif sys.platform != 'darwin': 56 | try: 57 | # ascii and utf-8 cannot encode the byte 0xff 58 | b'\xff'.decode(sys.getfilesystemencoding()) 59 | except UnicodeDecodeError: 60 | # 0xff will be encoded using the surrogate character u+DCFF 61 | TESTFN_UNENCODABLE = TESTFN_ASCII \ 62 | + b'-\xff'.decode(sys.getfilesystemencoding(), 'surrogateescape') 63 | else: 64 | # File system encoding (eg. ISO-8859-* encodings) can encode 65 | # the byte 0xff. Skip some unicode filename tests. 66 | pass 67 | 68 | # FS_NONASCII: non-ASCII character encodable by os.fsencode(), 69 | # or an empty string if there is no such character. 70 | FS_NONASCII = '' 71 | for character in ( 72 | # First try printable and common characters to have a readable filename. 73 | # For each character, the encoding list are just example of encodings able 74 | # to encode the character (the list is not exhaustive). 75 | 76 | # U+00E6 (Latin Small Letter Ae): cp1252, iso-8859-1 77 | '\u00E6', 78 | # U+0130 (Latin Capital Letter I With Dot Above): cp1254, iso8859_3 79 | '\u0130', 80 | # U+0141 (Latin Capital Letter L With Stroke): cp1250, cp1257 81 | '\u0141', 82 | # U+03C6 (Greek Small Letter Phi): cp1253 83 | '\u03C6', 84 | # U+041A (Cyrillic Capital Letter Ka): cp1251 85 | '\u041A', 86 | # U+05D0 (Hebrew Letter Alef): Encodable to cp424 87 | '\u05D0', 88 | # U+060C (Arabic Comma): cp864, cp1006, iso8859_6, mac_arabic 89 | '\u060C', 90 | # U+062A (Arabic Letter Teh): cp720 91 | '\u062A', 92 | # U+0E01 (Thai Character Ko Kai): cp874 93 | '\u0E01', 94 | 95 | # Then try more "special" characters. "special" because they may be 96 | # interpreted or displayed differently depending on the exact locale 97 | # encoding and the font. 98 | 99 | # U+00A0 (No-Break Space) 100 | '\u00A0', 101 | # U+20AC (Euro Sign) 102 | '\u20AC', 103 | ): 104 | try: 105 | # If Python is set up to use the legacy 'mbcs' in Windows, 106 | # 'replace' error mode is used, and encode() returns b'?' 107 | # for characters missing in the ANSI codepage 108 | if os.fsdecode(os.fsencode(character)) != character: 109 | raise UnicodeError 110 | except UnicodeError: 111 | pass 112 | else: 113 | FS_NONASCII = character 114 | break 115 | 116 | # Save the initial cwd 117 | SAVEDCWD = os.getcwd() 118 | 119 | # TESTFN_UNDECODABLE is a filename (bytes type) that should *not* be able to be 120 | # decoded from the filesystem encoding (in strict mode). It can be None if we 121 | # cannot generate such filename (ex: the latin1 encoding can decode any byte 122 | # sequence). On UNIX, TESTFN_UNDECODABLE can be decoded by os.fsdecode() thanks 123 | # to the surrogateescape error handler (PEP 383), but not from the filesystem 124 | # encoding in strict mode. 125 | TESTFN_UNDECODABLE = None 126 | for name in ( 127 | # b'\xff' is not decodable by os.fsdecode() with code page 932. Windows 128 | # accepts it to create a file or a directory, or don't accept to enter to 129 | # such directory (when the bytes name is used). So test b'\xe7' first: 130 | # it is not decodable from cp932. 131 | b'\xe7w\xf0', 132 | # undecodable from ASCII, UTF-8 133 | b'\xff', 134 | # undecodable from iso8859-3, iso8859-6, iso8859-7, cp424, iso8859-8, cp856 135 | # and cp857 136 | b'\xae\xd5' 137 | # undecodable from UTF-8 (UNIX and Mac OS X) 138 | b'\xed\xb2\x80', b'\xed\xb4\x80', 139 | # undecodable from shift_jis, cp869, cp874, cp932, cp1250, cp1251, cp1252, 140 | # cp1253, cp1254, cp1255, cp1257, cp1258 141 | b'\x81\x98', 142 | ): 143 | try: 144 | name.decode(sys.getfilesystemencoding()) 145 | except UnicodeDecodeError: 146 | TESTFN_UNDECODABLE = os.fsencode(TESTFN_ASCII) + name 147 | break 148 | 149 | if FS_NONASCII: 150 | TESTFN_NONASCII: Optional[str] = TESTFN_ASCII + FS_NONASCII 151 | else: 152 | TESTFN_NONASCII = None 153 | TESTFN = TESTFN_NONASCII or TESTFN_ASCII 154 | 155 | 156 | def make_bad_fd(): 157 | """ 158 | Create an invalid file descriptor by opening and closing a file and return 159 | its fd. 160 | """ 161 | file = open(TESTFN, "wb") 162 | try: 163 | return file.fileno() 164 | finally: 165 | file.close() 166 | unlink(TESTFN) 167 | 168 | 169 | _can_symlink = None 170 | 171 | 172 | def can_symlink(): 173 | global _can_symlink 174 | if _can_symlink is not None: 175 | return _can_symlink 176 | symlink_path = TESTFN + "can_symlink" 177 | try: 178 | os.symlink(TESTFN, symlink_path) 179 | can = True 180 | except (OSError, NotImplementedError, AttributeError): 181 | can = False 182 | else: 183 | os.remove(symlink_path) 184 | _can_symlink = can 185 | return can 186 | 187 | 188 | def skip_unless_symlink(test): 189 | """Skip decorator for tests that require functional symlink""" 190 | ok = can_symlink() 191 | msg = "Requires functional symlink implementation" 192 | return test if ok else unittest.skip(msg)(test) 193 | 194 | 195 | _can_xattr = None 196 | 197 | 198 | def can_xattr(): 199 | import tempfile 200 | global _can_xattr 201 | if _can_xattr is not None: 202 | return _can_xattr 203 | if not hasattr(os, "setxattr"): 204 | can = False 205 | else: 206 | import platform 207 | tmp_dir = tempfile.mkdtemp() 208 | tmp_fp, tmp_name = tempfile.mkstemp(dir=tmp_dir) 209 | try: 210 | with open(TESTFN, "wb") as fp: 211 | try: 212 | # TESTFN & tempfile may use different file systems with 213 | # different capabilities 214 | os.setxattr(tmp_fp, b"user.test", b"") 215 | os.setxattr(tmp_name, b"trusted.foo", b"42") 216 | os.setxattr(fp.fileno(), b"user.test", b"") 217 | # Kernels < 2.6.39 don't respect setxattr flags. 218 | kernel_version = platform.release() 219 | m = re.match(r"2.6.(\d{1,2})", kernel_version) 220 | can = m is None or int(m.group(1)) >= 39 221 | except OSError: 222 | can = False 223 | finally: 224 | unlink(TESTFN) 225 | unlink(tmp_name) 226 | rmdir(tmp_dir) 227 | _can_xattr = can 228 | return can 229 | 230 | 231 | def skip_unless_xattr(test): 232 | """Skip decorator for tests that require functional extended attributes""" 233 | ok = can_xattr() 234 | msg = "no non-broken extended attribute support" 235 | return test if ok else unittest.skip(msg)(test) 236 | 237 | 238 | def unlink(filename): 239 | try: 240 | _unlink(filename) 241 | except (FileNotFoundError, NotADirectoryError): 242 | pass 243 | 244 | 245 | if sys.platform.startswith("win"): 246 | def _waitfor(func, pathname, waitall=False): 247 | # Perform the operation 248 | func(pathname) 249 | # Now setup the wait loop 250 | if waitall: 251 | dirname = pathname 252 | else: 253 | dirname, name = os.path.split(pathname) 254 | dirname = dirname or '.' 255 | # Check for `pathname` to be removed from the filesystem. 256 | # The exponential backoff of the timeout amounts to a total 257 | # of ~1 second after which the deletion is probably an error 258 | # anyway. 259 | # Testing on an i7@4.3GHz shows that usually only 1 iteration is 260 | # required when contention occurs. 261 | timeout = 0.001 262 | while timeout < 1.0: 263 | # Note we are only testing for the existence of the file(s) in 264 | # the contents of the directory regardless of any security or 265 | # access rights. If we have made it this far, we have sufficient 266 | # permissions to do that much using Python's equivalent of the 267 | # Windows API FindFirstFile. 268 | # Other Windows APIs can fail or give incorrect results when 269 | # dealing with files that are pending deletion. 270 | L = os.listdir(dirname) 271 | if not (L if waitall else name in L): 272 | return 273 | # Increase the timeout and try again 274 | time.sleep(timeout) 275 | timeout *= 2 276 | warnings.warn('tests may fail, delete still pending for ' + pathname, 277 | RuntimeWarning, stacklevel=4) 278 | 279 | def _unlink(filename): 280 | _waitfor(os.unlink, filename) 281 | 282 | def _rmdir(dirname): 283 | _waitfor(os.rmdir, dirname) 284 | 285 | def _rmtree(path): 286 | from test.support import _force_run 287 | 288 | def _rmtree_inner(path): 289 | for name in _force_run(path, os.listdir, path): 290 | fullname = os.path.join(path, name) 291 | try: 292 | mode = os.lstat(fullname).st_mode 293 | except OSError as exc: 294 | print("support.rmtree(): os.lstat(%r) failed with %s" 295 | % (fullname, exc), 296 | file=sys.__stderr__) 297 | mode = 0 298 | if stat.S_ISDIR(mode): 299 | _waitfor(_rmtree_inner, fullname, waitall=True) 300 | _force_run(fullname, os.rmdir, fullname) 301 | else: 302 | _force_run(fullname, os.unlink, fullname) 303 | _waitfor(_rmtree_inner, path, waitall=True) 304 | _waitfor(lambda p: _force_run(p, os.rmdir, p), path) 305 | 306 | def _longpath(path): 307 | try: 308 | import ctypes 309 | except ImportError: 310 | # No ctypes means we can't expands paths. 311 | pass 312 | else: 313 | buffer = ctypes.create_unicode_buffer(len(path) * 2) 314 | length = ctypes.windll.kernel32.GetLongPathNameW(path, buffer, 315 | len(buffer)) 316 | if length: 317 | return buffer[:length] 318 | return path 319 | else: 320 | _unlink = os.unlink 321 | _rmdir = os.rmdir 322 | 323 | def _rmtree(path): 324 | import shutil 325 | try: 326 | shutil.rmtree(path) 327 | return 328 | except OSError: 329 | pass 330 | 331 | def _rmtree_inner(path): 332 | from test.support import _force_run 333 | for name in _force_run(path, os.listdir, path): 334 | fullname = os.path.join(path, name) 335 | try: 336 | mode = os.lstat(fullname).st_mode 337 | except OSError: 338 | mode = 0 339 | if stat.S_ISDIR(mode): 340 | _rmtree_inner(fullname) 341 | _force_run(path, os.rmdir, fullname) 342 | else: 343 | _force_run(path, os.unlink, fullname) 344 | _rmtree_inner(path) 345 | os.rmdir(path) 346 | 347 | def _longpath(path): 348 | return path 349 | 350 | 351 | def rmdir(dirname): 352 | try: 353 | _rmdir(dirname) 354 | except FileNotFoundError: 355 | pass 356 | 357 | 358 | def rmtree(path): 359 | try: 360 | _rmtree(path) 361 | except FileNotFoundError: 362 | pass 363 | 364 | 365 | @contextlib.contextmanager 366 | def temp_dir(path=None, quiet=False): 367 | """Return a context manager that creates a temporary directory. 368 | Arguments: 369 | path: the directory to create temporarily. If omitted or None, 370 | defaults to creating a temporary directory using tempfile.mkdtemp. 371 | quiet: if False (the default), the context manager raises an exception 372 | on error. Otherwise, if the path is specified and cannot be 373 | created, only a warning is issued. 374 | """ 375 | import tempfile 376 | dir_created = False 377 | if path is None: 378 | path = tempfile.mkdtemp() 379 | dir_created = True 380 | path = os.path.realpath(path) 381 | else: 382 | try: 383 | os.mkdir(path) 384 | dir_created = True 385 | except OSError as exc: 386 | if not quiet: 387 | raise 388 | warnings.warn(f'tests may fail, unable to create ' 389 | f'temporary directory {path!r}: {exc}', 390 | RuntimeWarning, stacklevel=3) 391 | if dir_created: 392 | pid = os.getpid() 393 | try: 394 | yield path 395 | finally: 396 | # In case the process forks, let only the parent remove the 397 | # directory. The child has a different process id. (bpo-30028) 398 | if dir_created and pid == os.getpid(): 399 | rmtree(path) 400 | 401 | 402 | @contextlib.contextmanager 403 | def change_cwd(path, quiet=False): 404 | """Return a context manager that changes the current working directory. 405 | Arguments: 406 | path: the directory to use as the temporary current working directory. 407 | quiet: if False (the default), the context manager raises an exception 408 | on error. Otherwise, it issues only a warning and keeps the current 409 | working directory the same. 410 | """ 411 | saved_dir = os.getcwd() 412 | try: 413 | os.chdir(os.path.realpath(path)) 414 | except OSError as exc: 415 | if not quiet: 416 | raise 417 | warnings.warn(f'tests may fail, unable to change the current working ' 418 | f'directory to {path!r}: {exc}', 419 | RuntimeWarning, stacklevel=3) 420 | try: 421 | yield os.getcwd() 422 | finally: 423 | os.chdir(saved_dir) 424 | 425 | 426 | @contextlib.contextmanager 427 | def temp_cwd(name='tempcwd', quiet=False): 428 | """ 429 | Context manager that temporarily creates and changes the CWD. 430 | The function temporarily changes the current working directory 431 | after creating a temporary directory in the current directory with 432 | name *name*. If *name* is None, the temporary directory is 433 | created using tempfile.mkdtemp. 434 | If *quiet* is False (default) and it is not possible to 435 | create or change the CWD, an error is raised. If *quiet* is True, 436 | only a warning is raised and the original CWD is used. 437 | """ 438 | with temp_dir(path=name, quiet=quiet) as temp_path: 439 | with change_cwd(temp_path, quiet=quiet) as cwd_dir: 440 | yield cwd_dir 441 | 442 | 443 | def create_empty_file(filename): 444 | """Create an empty file. If the file already exists, truncate it.""" 445 | fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) 446 | os.close(fd) 447 | 448 | 449 | def fs_is_case_insensitive(directory): 450 | """Detects if the file system for the specified directory 451 | is case-insensitive.""" 452 | import tempfile 453 | with tempfile.NamedTemporaryFile(dir=directory) as base: 454 | base_path = base.name 455 | case_path = base_path.upper() 456 | if case_path == base_path: 457 | case_path = base_path.lower() 458 | try: 459 | return os.path.samefile(base_path, case_path) 460 | except FileNotFoundError: 461 | return False 462 | 463 | 464 | class FakePath: 465 | """Simple implementing of the path protocol. 466 | """ 467 | def __init__(self, path): 468 | self.path = path 469 | 470 | def __repr__(self): 471 | return f'' 472 | 473 | def __fspath__(self): 474 | if (isinstance(self.path, BaseException) or 475 | isinstance(self.path, type) and 476 | issubclass(self.path, BaseException)): 477 | raise self.path 478 | else: 479 | return self.path 480 | 481 | 482 | def fd_count(): 483 | """Count the number of open file descriptors. 484 | """ 485 | if sys.platform.startswith(('linux', 'freebsd')): 486 | try: 487 | names = os.listdir("/proc/self/fd") 488 | # Subtract one because listdir() internally opens a file 489 | # descriptor to list the content of the /proc/self/fd/ directory. 490 | return len(names) - 1 491 | except FileNotFoundError: 492 | pass 493 | 494 | MAXFD = 256 495 | if hasattr(os, 'sysconf'): 496 | try: 497 | MAXFD = os.sysconf("SC_OPEN_MAX") 498 | except OSError: 499 | pass 500 | 501 | old_modes = None 502 | if sys.platform == 'win32': 503 | # bpo-25306, bpo-31009: Call CrtSetReportMode() to not kill the process 504 | # on invalid file descriptor if Python is compiled in debug mode 505 | try: 506 | import msvcrt 507 | msvcrt.CrtSetReportMode 508 | except (AttributeError, ImportError): 509 | # no msvcrt or a release build 510 | pass 511 | else: 512 | old_modes = {} 513 | for report_type in (msvcrt.CRT_WARN, 514 | msvcrt.CRT_ERROR, 515 | msvcrt.CRT_ASSERT): 516 | old_modes[report_type] = msvcrt.CrtSetReportMode(report_type, 517 | 0) 518 | 519 | try: 520 | count = 0 521 | for fd in range(MAXFD): 522 | try: 523 | # Prefer dup() over fstat(). fstat() can require input/output 524 | # whereas dup() doesn't. 525 | fd2 = os.dup(fd) 526 | except OSError as e: 527 | if e.errno != errno.EBADF: 528 | raise 529 | else: 530 | os.close(fd2) 531 | count += 1 532 | finally: 533 | if old_modes is not None: 534 | for report_type in (msvcrt.CRT_WARN, 535 | msvcrt.CRT_ERROR, 536 | msvcrt.CRT_ASSERT): 537 | msvcrt.CrtSetReportMode(report_type, old_modes[report_type]) 538 | 539 | return count 540 | 541 | 542 | if hasattr(os, "umask"): 543 | @contextlib.contextmanager 544 | def temp_umask(umask): 545 | """Context manager that temporarily sets the process umask.""" 546 | oldmask = os.umask(umask) 547 | try: 548 | yield 549 | finally: 550 | os.umask(oldmask) 551 | 552 | 553 | class EnvironmentVarGuard(collections.abc.MutableMapping): 554 | 555 | """Class to help protect the environment variable properly. Can be used as 556 | a context manager.""" 557 | 558 | def __init__(self): 559 | self._environ = os.environ 560 | self._changed = {} 561 | 562 | def __getitem__(self, envvar): 563 | return self._environ[envvar] 564 | 565 | def __setitem__(self, envvar, value): 566 | # Remember the initial value on the first access 567 | if envvar not in self._changed: 568 | self._changed[envvar] = self._environ.get(envvar) 569 | self._environ[envvar] = value 570 | 571 | def __delitem__(self, envvar): 572 | # Remember the initial value on the first access 573 | if envvar not in self._changed: 574 | self._changed[envvar] = self._environ.get(envvar) 575 | if envvar in self._environ: 576 | del self._environ[envvar] 577 | 578 | def keys(self): 579 | return self._environ.keys() 580 | 581 | def __iter__(self): 582 | return iter(self._environ) 583 | 584 | def __len__(self): 585 | return len(self._environ) 586 | 587 | def set(self, envvar, value): 588 | self[envvar] = value 589 | 590 | def unset(self, envvar): 591 | del self[envvar] 592 | 593 | def copy(self): 594 | # We do what os.environ.copy() does. 595 | return dict(self) 596 | 597 | def __enter__(self): 598 | return self 599 | 600 | def __exit__(self, *ignore_exc): 601 | for (k, v) in self._changed.items(): 602 | if v is None: 603 | if k in self._environ: 604 | del self._environ[k] 605 | else: 606 | self._environ[k] = v 607 | os.environ = self._environ -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /tests/test_private.py: -------------------------------------------------------------------------------- 1 | # some extra tests for coverage 2 | 3 | import pytest 4 | import os 5 | from pathlib2 import os_path_realpath, _make_selector, Path 6 | 7 | 8 | @pytest.mark.skipif(os.name != "nt", reason="Windows only test") 9 | def test_realpath_nt_nul(): 10 | assert os_path_realpath(b'nul') == b'\\\\.\\NUL' 11 | assert os_path_realpath('nul') == '\\\\.\\NUL' 12 | 13 | 14 | @pytest.mark.skipif(os.name != "nt", reason="Windows only test") 15 | def test_realpath_nt_unc(): 16 | assert os_path_realpath('\\\\?\\UNC\\localhost\\C$') == '\\\\?\\UNC\\localhost\\C$' 17 | 18 | 19 | @pytest.mark.skipif(os.name != "nt", reason="Windows only test") 20 | def test_realpath_nt_badpath(): 21 | with pytest.raises(FileNotFoundError): 22 | os_path_realpath('\\\\invalid\\server') 23 | with pytest.raises(FileNotFoundError): 24 | os_path_realpath('does/not/exist', strict=True) 25 | 26 | 27 | def test_realpath_bytes(): 28 | assert os_path_realpath(b'abc.xyz').endswith(b'abc.xyz') 29 | 30 | 31 | def test_make_selector(): 32 | with pytest.raises(ValueError, match="Invalid pattern"): 33 | _make_selector(("x**x",), None) 34 | 35 | 36 | def test_parents_repr(): 37 | p = Path("/some/path/here") 38 | assert repr(p.parents).endswith(".parents>") 39 | 40 | 41 | def test_bad_glob(): 42 | p = Path("some/path") 43 | files = p.glob("/test/**") 44 | with pytest.raises(NotImplementedError, 45 | match="Non-relative patterns are unsupported"): 46 | next(files) 47 | 48 | 49 | @pytest.mark.skipif(os.name != "nt", reason="only for windows") 50 | def test_is_mount_windows(): 51 | p = Path("some/path") 52 | with pytest.raises(NotImplementedError): 53 | p.is_mount() 54 | -------------------------------------------------------------------------------- /tests/test_unicode.py: -------------------------------------------------------------------------------- 1 | """Test pathlib's handling of Unicode strings.""" 2 | 3 | import errno 4 | import shutil 5 | import tempfile 6 | 7 | import pytest 8 | 9 | import pathlib2 as pathlib 10 | 11 | from tests.os_helper import skip_unless_symlink 12 | 13 | import unittest 14 | 15 | 16 | class TestUnicode(unittest.TestCase): 17 | """Test the Unicode strings handling of various pathlib2 methods.""" 18 | 19 | # pylint: disable=too-many-public-methods 20 | 21 | def examine_path(self, path, name): 22 | # type: (TestUnicode, pathlib.Path, str) -> None 23 | """Examine an already-built path.""" 24 | msg = "examining {path!r}: {parts!r}, expect name {name!r}".format( 25 | path=path, 26 | parts=path.parts, 27 | name=name, 28 | ) 29 | self.assertEqual(path.name, name, msg) 30 | self.assertIsInstance(path.name, str, msg) 31 | self.assertTrue(all(isinstance(part, str) for part in path.parts), msg) 32 | 33 | def setUp(self): 34 | # type: (TestUnicode) -> None 35 | """Create a temporary directory and a single file in it.""" 36 | self.tempd = tempfile.mkdtemp(prefix="pathlib-test.") 37 | tempd_etc_b = str(self.tempd).encode("UTF-8") + b"/etc" 38 | self.base = pathlib.Path(tempd_etc_b.decode("UTF-8")) 39 | self.base.mkdir(0o755) 40 | (self.base / "a.file").write_bytes(b"") 41 | 42 | def tearDown(self): 43 | # type: (TestUnicode) -> None 44 | """Remove the temporary directory.""" 45 | shutil.rmtree(self.tempd) 46 | 47 | def get_child(self): 48 | # type: (TestUnicode) -> pathlib.Path 49 | """Get a /etc/passwd path with a Unicode last component.""" 50 | return self.base / b"passwd".decode("UTF-8") 51 | 52 | def test_init(self): 53 | # type: (TestUnicode) -> None 54 | """Test that self.base was constructed properly.""" 55 | self.examine_path(self.base, "etc") 56 | 57 | def test_absolute(self): 58 | # type: (TestUnicode) -> None 59 | """Test that pathlib.Path.absolute() returns str objects only.""" 60 | self.examine_path(self.base.absolute(), "etc") 61 | 62 | def test_anchor(self): 63 | # type: (TestUnicode) -> None 64 | """Test that pathlib.Path.anchor() returns a str object.""" 65 | self.assertIsInstance(self.base.anchor, str) 66 | 67 | def test_glob(self): 68 | # type: (TestUnicode) -> None 69 | """Test that pathlib.Path.glob() accepts a Unicode pattern.""" 70 | first_child = next(self.base.glob(b"*".decode("us-ascii"))) 71 | self.examine_path(first_child, "a.file") 72 | 73 | def test_div(self): 74 | # type: (TestUnicode) -> None 75 | """Test that div/truediv/rtruediv accepts a Unicode argument.""" 76 | child = self.get_child() 77 | self.examine_path(child, "passwd") 78 | 79 | def test_joinpath(self): 80 | # type: (TestUnicode) -> None 81 | """Test that pathlib.Path.joinpath() accepts a Unicode path.""" 82 | child = self.get_child() 83 | self.examine_path(child, "passwd") 84 | 85 | def test_match(self): 86 | # type: (TestUnicode) -> None 87 | """Test that pathlib.Path.match() accepts a Unicode pattern.""" 88 | self.assertTrue(self.base.match(b"*etc".decode("us-ascii"))) 89 | 90 | def test_parent(self): 91 | # type: (TestUnicode) -> None 92 | """Test that pathlib.Path.parent() returns str objects.""" 93 | child = self.get_child() 94 | self.examine_path(child.parent, "etc") 95 | 96 | def test_parents(self): 97 | # type: (TestUnicode) -> None 98 | """Test that pathlib.Path.parent() returns str objects.""" 99 | child = self.get_child() 100 | for parent in child.parents: 101 | if str(parent) != "/": 102 | self.examine_path(parent, parent.name) 103 | 104 | def test_relative_to(self): 105 | # type: (TestUnicode) -> None 106 | """Test that pathlib.Path.relative_to() accepts a Unicode path.""" 107 | child = self.get_child() 108 | rel = child.relative_to( 109 | str(child.parent).encode("UTF-8").decode("UTF-8") 110 | ) 111 | self.examine_path(rel, "passwd") 112 | 113 | def test_rename(self): 114 | # type: (TestUnicode) -> None 115 | """Test that pathlib.Path.rename() accepts a Unicode path.""" 116 | first_child = next(self.base.glob(b"*".decode("us-ascii"))) 117 | with pytest.raises(OSError) as err_info: 118 | first_child.rename(b"/nonexistent/nah".decode("us-ascii")) 119 | assert err_info.value.errno in {errno.ENOENT, errno.ENOTDIR} 120 | 121 | def test_replace(self): 122 | # type: (TestUnicode) -> None 123 | """Test that pathlib.Path.replace() accepts a Unicode path.""" 124 | first_child = next(self.base.glob(b"*".decode("us-ascii"))) 125 | with pytest.raises(OSError) as err_info: 126 | first_child.replace(b"/nonexistent/nah".decode("us-ascii")) 127 | assert err_info.value.errno in {errno.ENOENT, errno.ENOTDIR} 128 | 129 | def test_rglob(self): 130 | # type: (TestUnicode) -> None 131 | """Test that pathlib.Path.rglob() accepts a Unicode pattern.""" 132 | first_child = next(self.base.rglob(b"*".decode("us-ascii"))) 133 | self.examine_path(first_child, "a.file") 134 | 135 | def test_root(self): 136 | # type: (TestUnicode) -> None 137 | """Test that pathlib.Path.root returns a str object.""" 138 | child = self.get_child() 139 | self.assertIsInstance(child.root, str) 140 | 141 | def test_samefile(self): 142 | # type: (TestUnicode) -> None 143 | """Test that pathlib.Path.samefile() accepts a Unicode path.""" 144 | first_child = next(self.base.rglob(b"*".decode("us-ascii"))) 145 | self.assertFalse( 146 | first_child.samefile(str(__file__).encode("UTF-8").decode("UTF-8")) 147 | ) 148 | 149 | def test_stem(self): 150 | # type: (TestUnicode) -> None 151 | """Test that pathlib.Path.stem returns a str object.""" 152 | child = self.get_child() 153 | self.assertIsInstance(child.stem, str) 154 | 155 | def test_suffix(self): 156 | # type: (TestUnicode) -> None 157 | """Test that pathlib.Path.suffix returns a str object.""" 158 | child = self.get_child() 159 | self.assertIsInstance(child.suffix, str) 160 | 161 | def test_suffixes(self): 162 | # type: (TestUnicode) -> None 163 | """Test that pathlib.Path.suffixes returns str objects.""" 164 | child = self.get_child() 165 | self.assertTrue( 166 | all(isinstance(suffix, str) for suffix in child.suffixes) 167 | ) 168 | 169 | @skip_unless_symlink 170 | def test_symlink_to(self): 171 | # type: (TestUnicode) -> None 172 | """Test that pathlib.Path.symlink_to() accepts a Unicode path.""" 173 | child = self.get_child() 174 | first_child = next(self.base.rglob(b"*".decode("us-ascii"))) 175 | self.assertFalse(child.exists(), repr(child.parts)) 176 | child.symlink_to(b"a.file".decode("us-ascii")) 177 | self.assertTrue( 178 | child.samefile(first_child), 179 | repr((child.parts, first_child.parts)), 180 | ) 181 | 182 | def test_with_name(self): 183 | # type: (TestUnicode) -> None 184 | """Test that pathlib.Path.with_name() accepts a Unicode name.""" 185 | child = self.get_child() 186 | child = child.with_name(b"hosts".decode("us-ascii")) 187 | self.examine_path(child, "hosts") 188 | 189 | def test_with_suffix(self): 190 | # type: (TestUnicode) -> None 191 | """Test that pathlib.Path.with_suffix() accepts a Unicode suffix.""" 192 | child = self.get_child() 193 | child = child.with_suffix(b".txt".decode("us-ascii")) 194 | self.examine_path(child, "passwd.txt") 195 | --------------------------------------------------------------------------------