├── .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 |
--------------------------------------------------------------------------------