From 29d9ebee0154e77e416162061752833410e98cbd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2016 09:32:33 -0400 Subject: [PATCH 001/835] Update comment to reflect the Github-backed skeleton model (preferred to the generation library-backed model). --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 99d6c9c9..91e4110c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Generated by jaraco.develop 2.27.1 -# https://pypi.python.org/pypi/jaraco.develop + +# Project skeleton maintained at https://github.com/jaraco/skeleton import io import sys From b93b3a0348e9a17ec323f74eb9eb0ec8e82367ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 May 2016 12:21:58 -0400 Subject: [PATCH 002/835] Exclude the skeleton branch from testing --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index c5f3495b..bb6d47e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ python: script: - pip install -U pytest - python setup.py test +branches: + except: + - skeleton deploy: provider: pypi on: From 9d74152ecc4375b36a82ecdec01847ffd4b0cc45 Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Thu, 28 Jul 2016 22:01:06 +0200 Subject: [PATCH 003/835] Only call _always_unicode when not PY3 --- path.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/path.py b/path.py index 1e92a490..34a4a0f6 100644 --- a/path.py +++ b/path.py @@ -220,7 +220,7 @@ def _always_unicode(cls, path): Ensure the path as retrieved from a Python API, such as :func:`os.listdir`, is a proper Unicode string. """ - if PY3 or isinstance(path, text_type): + if isinstance(path, text_type): return path return path.decode(sys.getfilesystemencoding(), 'surrogateescape') @@ -531,9 +531,14 @@ def listdir(self, pattern=None): """ if pattern is None: pattern = '*' + + children = os.listdir(self) + if not PY3: + children = map(self._always_unicode, children) + return [ self / child - for child in map(self._always_unicode, os.listdir(self)) + for child in children if self._next_class(child).fnmatch(pattern) ] From 960939765d6419b5955100b1a85f155812337689 Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Thu, 28 Jul 2016 22:05:32 +0200 Subject: [PATCH 004/835] Make Path.listdir omit pattern matching when no pattern --- path.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/path.py b/path.py index 34a4a0f6..d27e9e94 100644 --- a/path.py +++ b/path.py @@ -529,13 +529,13 @@ def listdir(self, pattern=None): .. seealso:: :meth:`files`, :meth:`dirs` """ - if pattern is None: - pattern = '*' - children = os.listdir(self) if not PY3: children = map(self._always_unicode, children) + if pattern is None: + return [self / child for child in children] + return [ self / child for child in children From b1a24617eca0863b317c479fb669cd2a8d932d8d Mon Sep 17 00:00:00 2001 From: Nils Maier Date: Thu, 28 Jul 2016 22:31:42 +0200 Subject: [PATCH 005/835] Prepare and reuse fnmatch patterns in listdir() and walk*() --- path.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/path.py b/path.py index d27e9e94..31707f00 100644 --- a/path.py +++ b/path.py @@ -536,10 +536,11 @@ def listdir(self, pattern=None): if pattern is None: return [self / child for child in children] + pattern, normcase = self._prepare_fnmatch_pattern(pattern) return [ self / child for child in children - if self._next_class(child).fnmatch(pattern) + if self._next_class(child)._prepared_fnmatch(pattern, normcase) ] def dirs(self, pattern=None): @@ -598,6 +599,15 @@ def ignore(msg): raise ValueError("invalid errors parameter") errors = vars(Handlers).get(errors, errors) + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walk(pattern, normcase, errors) + + def _prepared_walk(self, pattern, normcase, errors): + """ Prepared version of walk """ try: childList = self.listdir() except Exception: @@ -608,7 +618,7 @@ def ignore(msg): return for child in childList: - if pattern is None or child.fnmatch(pattern): + if pattern is None or child._prepared_fnmatch(pattern, normcase): yield child try: isdir = child.isdir() @@ -620,7 +630,7 @@ def ignore(msg): isdir = False if isdir: - for item in child.walk(pattern, errors): + for item in child._prepared_walk(pattern, normcase, errors): yield item def walkdirs(self, pattern=None, errors='strict'): @@ -639,6 +649,15 @@ def walkdirs(self, pattern=None, errors='strict'): if errors not in ('strict', 'warn', 'ignore'): raise ValueError("invalid errors parameter") + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walkdirs(pattern, normcase, errors) + + def _prepared_walkdirs(self, pattern, normcase, errors): + """ Prepared version of walkdirs """ try: dirs = self.dirs() except Exception: @@ -654,9 +673,9 @@ def walkdirs(self, pattern=None, errors='strict'): raise for child in dirs: - if pattern is None or child.fnmatch(pattern): + if pattern is None or child._prepared_fnmatch(pattern, normcase): yield child - for subsubdir in child.walkdirs(pattern, errors): + for subsubdir in child._prepared_walkdirs(pattern, normcase, errors): yield subsubdir def walkfiles(self, pattern=None, errors='strict'): @@ -670,6 +689,15 @@ def walkfiles(self, pattern=None, errors='strict'): if errors not in ('strict', 'warn', 'ignore'): raise ValueError("invalid errors parameter") + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walkfiles(pattern, normcase, errors) + + def _prepared_walkfiles(self, pattern, normcase, errors): + """ Prepared version of walkfiles """ try: childList = self.listdir() except Exception: @@ -701,12 +729,45 @@ def walkfiles(self, pattern=None, errors='strict'): raise if isfile: - if pattern is None or child.fnmatch(pattern): + if pattern is None or child._prepared_fnmatch(pattern, normcase): yield child elif isdir: - for f in child.walkfiles(pattern, errors): + for f in child._prepared_walkfiles(pattern, normcase, errors): yield f + def _prepared_fnmatch(self, pattern, normcase): + """ Return ``True`` if `self.name` matches the given `pattern`, + prepared version. + + `pattern` - A filename pattern with wildcards, + for example ``'*.py'``. The pattern is expected to be normcase'd + already. + + `normcase` - A function used to normalize the pattern and + filename before matching. + + .. seealso:: :func:`Path.fnmatch` + """ + return fnmatch.fnmatchcase(normcase(self.name), pattern) + + def _prepare_fnmatch_pattern(self, pattern, normcase=None): + """ Prepares a fmatch_pattern for use with ``Path._prepared_fnmatch`. + + `pattern` - A filename pattern with wildcards, + for example ``'*.py'``. If the pattern contains a `normcase` + attribute, it is applied to the name and path prior to comparison. + + `normcase` - (optional) A function used to normalize the pattern and + filename before matching. Defaults to :meth:`self.module`, which defaults + to :meth:`os.path.normcase`. + + .. seealso:: :func:`Path._prepared_fnmatch` + """ + if not normcase: + normcase = getattr(pattern, 'normcase', self.module.normcase) + pattern = normcase(pattern) + return pattern, normcase + def fnmatch(self, pattern, normcase=None): """ Return ``True`` if `self.name` matches the given `pattern`. @@ -720,11 +781,11 @@ def fnmatch(self, pattern, normcase=None): .. seealso:: :func:`fnmatch.fnmatch` """ - default_normcase = getattr(pattern, 'normcase', self.module.normcase) - normcase = normcase or default_normcase - name = normcase(self.name) - pattern = normcase(pattern) - return fnmatch.fnmatchcase(name, pattern) + if not pattern: + raise ValueError("No pattern provided") + + pattern, normcase = self._prepare_fnmatch_pattern(pattern, normcase) + return self._prepared_fnmatch(pattern, normcase) def glob(self, pattern): """ Return a list of Path objects that match the pattern. From efa552e7ee31d0fb1dab3d1a2986cad0834b04f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2016 09:51:36 -0400 Subject: [PATCH 006/835] Add badges for PyPI, downloads, and Travis-CI. --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 217a0758..e739bf50 100644 --- a/README.rst +++ b/README.rst @@ -1,2 +1,11 @@ skeleton ======== + +.. image:: https://badge.fury.io/py/skeleton.svg + :target: https://badge.fury.io/py/skeleton + +.. image:: https://pypip.in/d/skeleton/badge.png + :target: https://crate.io/packages/skeleton/ + +.. image:: https://secure.travis-ci.org/jaraco/skeleton.png + :target: http://travis-ci.org/jaraco/skeleton From e2900e901e9c24eb7ebf59792dc198bf0bd27cc8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2016 09:54:22 -0400 Subject: [PATCH 007/835] Change indentation to match that which the travis tool generates when adding the password. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb6d47e0..4abbe308 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ script: - python setup.py test branches: except: - - skeleton + - skeleton deploy: provider: pypi on: From c8c034e68873e40ed55f0b9f04afc5949eb54727 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2016 10:07:15 -0400 Subject: [PATCH 008/835] Use shields.io, as some of these other providers seem to have gone out of business. --- README.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index e739bf50..33249644 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,10 @@ skeleton ======== -.. image:: https://badge.fury.io/py/skeleton.svg - :target: https://badge.fury.io/py/skeleton +.. image:: https://img.shields.io/pypi/v/skeleton.svg + :target: https://pypi.io/project/skeleton -.. image:: https://pypip.in/d/skeleton/badge.png - :target: https://crate.io/packages/skeleton/ +.. image:: https://img.shields.io/pypi/dm/skeleton.svg -.. image:: https://secure.travis-ci.org/jaraco/skeleton.png - :target: http://travis-ci.org/jaraco/skeleton +.. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg + :target: http://travis-ci.org/jaraco/skeleton From 3f61a73b657a7a845f0f7fdbcebbf92c7f8e6c22 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2016 10:13:55 -0400 Subject: [PATCH 009/835] Also add pyversions --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 33249644..db95581e 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,8 @@ skeleton .. image:: https://img.shields.io/pypi/v/skeleton.svg :target: https://pypi.io/project/skeleton +.. image:: https://img.shields.io/pypi/pyversions/skeleton.svg + .. image:: https://img.shields.io/pypi/dm/skeleton.svg .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg From dfb1a9424d373fb2f949f2d45f79d8008ede276b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Aug 2016 14:49:32 -0400 Subject: [PATCH 010/835] Path is now .org --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index db95581e..75c0b4f0 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ skeleton ======== .. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: https://pypi.io/project/skeleton + :target: https://pypi.org/project/skeleton .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg From 7edaa321dead30e33accdb7512f9e95bbef9fe38 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Aug 2016 09:50:49 -0400 Subject: [PATCH 011/835] Update release process to use warehouse rather than legacy PyPI. Ref pypa/warehouse#1422. --- .travis.yml | 3 ++- setup.cfg | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4abbe308..9f4c5178 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,10 +11,11 @@ branches: - skeleton deploy: provider: pypi + server: https://upload.pypi.org/legacy/ on: tags: true all_branches: true python: 3.5 user: jaraco # supply password with `travis encrypt --add deploy.password` - distributions: release + distributions: dists diff --git a/setup.cfg b/setup.cfg index 8004dcb6..dcd8d122 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,10 @@ [aliases] -release = clean --all sdist bdist_wheel build_sphinx upload upload_docs +release = dists build_sphinx upload upload_docs +dists = clean --all sdist bdist_wheel test = pytest [wheel] universal = 1 + +[upload] +repository = https://upload.pypi.org/legacy/ From d024388cac7d3804c763e6f5656e75a6bde7d33c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Aug 2016 09:51:41 -0400 Subject: [PATCH 012/835] The name of the project need not be in the README --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 75c0b4f0..1c5d10ec 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,3 @@ -skeleton -======== - .. image:: https://img.shields.io/pypi/v/skeleton.svg :target: https://pypi.org/project/skeleton From 629d80f45dedc801e3fe19215ba50114b4c7b949 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Sep 2016 13:06:52 -0400 Subject: [PATCH 013/835] No need for a .gitignore file; projects may want to add one, but I recommend not having one unless the project has project-specific files to ignore. --- .gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e69de29b..00000000 From 03c1cc86843bcfbb6c2a9366d285427ac006aeee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Sep 2016 21:00:50 -0400 Subject: [PATCH 014/835] Remove support for building docs, now that docs support for pypi is deprecated. I hope at some point RTD comes up with an API that once again allows automatic building of docs. --- setup.cfg | 2 +- setup.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index dcd8d122..f5ee6072 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [aliases] -release = dists build_sphinx upload upload_docs +release = dists upload dists = clean --all sdist bdist_wheel test = pytest diff --git a/setup.py b/setup.py index 91e4110c..1f815f2e 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,7 @@ needs_pytest = {'pytest', 'test'}.intersection(sys.argv) pytest_runner = ['pytest_runner'] if needs_pytest else [] -needs_sphinx = {'release', 'build_sphinx', 'upload_docs'}.intersection(sys.argv) -sphinx = ['sphinx', 'rst.linker'] if needs_sphinx else [] -needs_wheel = {'release', 'bdist_wheel'}.intersection(sys.argv) +needs_wheel = {'release', 'bdist_wheel', 'dists'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] name = 'skeleton' @@ -37,7 +35,7 @@ }, setup_requires=[ 'setuptools_scm>=1.9', - ] + pytest_runner + sphinx + wheel, + ] + pytest_runner + wheel, tests_require=[ 'pytest>=2.8', ], From 750a2b38964adc868b1a7f4570afa1532418b12c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Sep 2016 10:50:07 -0500 Subject: [PATCH 015/835] Use tox instead of pytest-runner --- .travis.yml | 3 +-- pytest.ini | 2 +- setup.cfg | 1 - setup.py | 7 +------ tests/requirements.txt | 1 + tox.ini | 5 +++++ 6 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 tests/requirements.txt create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 9f4c5178..6c9a2cff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ python: - 2.7 - 3.5 script: -- pip install -U pytest -- python setup.py test +- tox branches: except: - skeleton diff --git a/pytest.ini b/pytest.ini index 9752c365..56a87745 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] -norecursedirs=*.egg .eggs dist build +norecursedirs=dist build .tox addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS diff --git a/setup.cfg b/setup.cfg index f5ee6072..4659acce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,6 @@ [aliases] release = dists upload dists = clean --all sdist bdist_wheel -test = pytest [wheel] universal = 1 diff --git a/setup.py b/setup.py index 1f815f2e..ec848c10 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,6 @@ with io.open('README.rst', encoding='utf-8') as readme: long_description = readme.read() -needs_pytest = {'pytest', 'test'}.intersection(sys.argv) -pytest_runner = ['pytest_runner'] if needs_pytest else [] needs_wheel = {'release', 'bdist_wheel', 'dists'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] @@ -35,10 +33,7 @@ }, setup_requires=[ 'setuptools_scm>=1.9', - ] + pytest_runner + wheel, - tests_require=[ - 'pytest>=2.8', - ], + ] + wheel, classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..70bc02f1 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pytest >= 2.8 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..dea83741 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[testenv] +deps = + -r tests/requirements.txt + +commands = py.test From cc80be915b6912056990bf71324826e244432533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Sep 2016 10:07:39 -0500 Subject: [PATCH 016/835] Use pkg_resources to resolve the version. Requires that the necessary package metadata have been built before building docs. --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9c7ad1b0..5abe25ae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import setuptools_scm +import pkg_resources extensions = [ 'sphinx.ext.autodoc', @@ -13,7 +13,7 @@ copyright = '2016 Jason R. Coombs' # The short X.Y version. -version = setuptools_scm.get_version(root='..', relative_to=__file__) +version = pkg_resources.require(project)[0].version # The full version, including alpha/beta/rc tags. release = version From 8b4139a8132c330623631f84528a3cd8f186df9a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Sep 2016 10:08:41 -0500 Subject: [PATCH 017/835] Each requirement line is passed as a single parameter to pip, so you can't have a space separating the option and its value. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dea83741..fa7284b8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [testenv] deps = - -r tests/requirements.txt + -rtests/requirements.txt commands = py.test From 4d382b3dee98d155f4057759ff015c3b6f0a15ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Sep 2016 12:29:05 -0400 Subject: [PATCH 018/835] Python Packaging -- never do with one command what you can do with two. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6c9a2cff..d7871d87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: python python: - 2.7 - 3.5 +install: +- pip install tox script: - tox branches: From 12196ba3c3e116a2514ed9fd22c6ed60539e9160 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2016 15:52:25 -0400 Subject: [PATCH 019/835] Provide a reference to the license declaration in the readme. Fixes jaraco/skeleton#1. --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 1c5d10ec..bb3d9127 100644 --- a/README.rst +++ b/README.rst @@ -7,3 +7,7 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: http://travis-ci.org/jaraco/skeleton + +License is indicated in the project metadata (typically one or more +of the Trove classifiers). For more details, see `this explanation +`_. From c4fd3f3cf414e2ee08ad53bd71cf9c201c69ca6f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2016 16:14:13 -0400 Subject: [PATCH 020/835] Use usedevelop to workaround tox-dev/tox#373 --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index fa7284b8..564f2050 100644 --- a/tox.ini +++ b/tox.ini @@ -3,3 +3,4 @@ deps = -rtests/requirements.txt commands = py.test +usedevelop = True From 96984072229ae07471373da73cad377a0cb324ef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 2 Oct 2016 09:28:19 -0500 Subject: [PATCH 021/835] Incorporate pre-release of setuptools to cause releases to include the PEP-420 deferral. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d7871d87..87881dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - 3.5 install: - pip install tox +- pip install https://github.com/pypa/setuptools/releases/download/v28.2.0b1/setuptools-28.2.0b1-py2.py3-none-any.whl script: - tox branches: From 1d7afbebd6530015d76ce93d88aa7a7c48c29717 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Oct 2016 12:16:38 -0700 Subject: [PATCH 022/835] Just upgrade to released setuptools now. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87881dbc..0c33b1ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ python: - 2.7 - 3.5 install: -- pip install tox -- pip install https://github.com/pypa/setuptools/releases/download/v28.2.0b1/setuptools-28.2.0b1-py2.py3-none-any.whl +- pip install tox "setuptools>=28.2" script: - tox branches: From 9be6e615930bdecb69cf4da887eefd0d53c425bd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Oct 2016 21:40:51 -0400 Subject: [PATCH 023/835] Exclude versions of setuptools_scm due to pypa/setuptools_scm#109. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ec848c10..27ace5fd 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ extras_require={ }, setup_requires=[ - 'setuptools_scm>=1.9', + 'setuptools_scm>=1.9,!=1.13.1,!=1.14.0', ] + wheel, classifiers=[ "Development Status :: 5 - Production/Stable", From aa1f8ebe0d2d3f49a36535b61824f2fece3bdd46 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Oct 2016 23:03:42 -0400 Subject: [PATCH 024/835] Allow passing posargs --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 564f2050..d740130c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,5 @@ deps = -rtests/requirements.txt -commands = py.test +commands = py.test {posargs} usedevelop = True From 60c7c186c133551cf0637354a642e49406f814b7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Oct 2016 20:16:20 -0400 Subject: [PATCH 025/835] Need a later version of setuptools_scm until it's released. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0c33b1ce..0a6cb29f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ script: branches: except: - skeleton +before_deploy: +- pip install https://dl.dropboxusercontent.com/u/54081/cheeseshop/setuptools_scm-1.14.1b1.tar.gz deploy: provider: pypi server: https://upload.pypi.org/legacy/ From 42ecbe7706cd756c5c3dff103fa3ff65e8a02349 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Oct 2016 15:38:47 -0400 Subject: [PATCH 026/835] Update to setuptools_scm 1.15.0rc1 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0a6cb29f..6effc440 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ branches: except: - skeleton before_deploy: -- pip install https://dl.dropboxusercontent.com/u/54081/cheeseshop/setuptools_scm-1.14.1b1.tar.gz +- pip install https://github.com/pypa/setuptools_scm/archive/v1.15.0rc1.tar.gz deploy: provider: pypi server: https://upload.pypi.org/legacy/ From 95fd34c61f8d9df2e9c559b3978c85e7d03cd8d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Oct 2016 17:13:15 -0400 Subject: [PATCH 027/835] Gotta get an sdist - so use one jaraco built --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6effc440..cad38c8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ branches: except: - skeleton before_deploy: -- pip install https://github.com/pypa/setuptools_scm/archive/v1.15.0rc1.tar.gz +- pip install https://dl.dropboxusercontent.com/u/54081/cheeseshop/setuptools_scm-1.15.0rc1.tar.gz deploy: provider: pypi server: https://upload.pypi.org/legacy/ From 200e6a525161b355d37862c9aee22c84e1413af4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 Oct 2016 10:07:33 -0400 Subject: [PATCH 028/835] Bump to setuptools_scm 1.15.0. --- .travis.yml | 2 -- setup.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index cad38c8f..0c33b1ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,6 @@ script: branches: except: - skeleton -before_deploy: -- pip install https://dl.dropboxusercontent.com/u/54081/cheeseshop/setuptools_scm-1.15.0rc1.tar.gz deploy: provider: pypi server: https://upload.pypi.org/legacy/ diff --git a/setup.py b/setup.py index 27ace5fd..83a22f68 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ extras_require={ }, setup_requires=[ - 'setuptools_scm>=1.9,!=1.13.1,!=1.14.0', + 'setuptools_scm>=1.15.0', ] + wheel, classifiers=[ "Development Status :: 5 - Production/Stable", From 4ee40ca2d13c2c8b544ad5f880193f5c0864648a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Nov 2016 09:49:30 -0400 Subject: [PATCH 029/835] Update config to support building on ReadTheDocs --- docs/conf.py | 2 +- docs/requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 5abe25ae..aa34defc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ master_doc = 'index' link_files = { - 'CHANGES.rst': dict( + '../CHANGES.rst': dict( using=dict( GH='https://github.com', project=project, diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..442df9fa --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +rst.linker From 18cb65f8f1c1eaf7b79a33fb5a2b7cbd1f851868 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Nov 2016 10:16:12 -0400 Subject: [PATCH 030/835] Add note about the broken docs problem. --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index bb3d9127..5196a108 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,20 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: http://travis-ci.org/jaraco/skeleton + +License +======= + License is indicated in the project metadata (typically one or more of the Trove classifiers). For more details, see `this explanation `_. + +Docs +==== + +There's `no good mechanism for publishing documentation +`_ +easily. If there's a documentation link above, it's probably +stale because PyPI-based documentation is deprecated. This +project may have documentation published at ReadTheDocs, but +probably not. Good luck finding it. From 193fe0b9b014bc0feb6d0d099952d1f0ef594efd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 10:50:52 -0500 Subject: [PATCH 031/835] Always use map from the future --- path.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/path.py b/path.py index 1e92a490..fd45823f 100644 --- a/path.py +++ b/path.py @@ -52,6 +52,7 @@ import io from distutils import dir_util import importlib +import itertools try: import win32security @@ -93,6 +94,7 @@ def surrogate_escape(error): text_type = __builtin__.unicode getcwdu = os.getcwdu codecs.register_error('surrogateescape', surrogate_escape) + map = itertools.imap @contextlib.contextmanager def io_error_compat(): From 177dc36824a43355b5a795a3b3cf613e46c68c70 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 11:36:54 -0500 Subject: [PATCH 032/835] Remove Mercurial metadata --- .hgtags | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .hgtags diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 05ad6c4f..00000000 --- a/.hgtags +++ /dev/null @@ -1,38 +0,0 @@ -9401857d62dd5d4784dc06656ab13992847b5e76 2.4 -69851aaf75f1c6ff63c72daf8d1a82692e04b301 2.4.1 -6ba869f2260f54ac6d66ffc08b9ce546846c6c9c 2.5 -5a008a763604cd3237ce83bb402fca41e4456001 2.6 -be871d26bce9185c8a4b2c20314f541508e99c1f 2.6.1 -5b78897bb40297548a7a01b35ce842eaa30d19de 3.0 -a5539b0050d4377bd1bdeb74eed06e0e4a775a99 3.0.1 -137d1b8b6d4ca80382efeda217d9236d1570ab70 3.1 -13f2731fa9d98314aedb2fdd55cce97b381b564f 3.2 -97899c3ef1421a247b0864f5486dd480ad31b41f 4.0 -9652fb4e2b486d2ce32f7ed3806304d1c0d653f7 4.1 -f922c0423dbf87f11643724ce3b7c2bc78303a8d 4.2 -38282c28271f66ed07d3c9b52bfa55e26280833d 4.3 -2ed42cef0b080079ad26de6f082fa6d722813087 4.4 -47206e8c59c858e074f6ea1ba80cefb3a37045fd 5.0 -0382bd2f0348cfd73e4e09cd55799a428e75a131 5.1 -7bf7fe39edc888b2f14faf76a70c75b5b17502e6 5.2 -83dcd6b47278aedcbf52f1a6798bd333c515b818 5.3 -2d5f08983462a86cdd41963fca6626ff87dc3ba1 6.0 -55c924ecc419dbf1038254436be512c5cb18bd15 6.1 -be72beb60a11f64344cd9e4c6029b330d49cd830 6.2 -698ac3acb84034fb17487b5009153c8956c5d596 7.0 -793192a17ea0e78640a6d6b133a2948f6cecf43f 7.1 -abeebd8675eaa4626619cb3083c8e1af48a2f2ce 7.2 -581040c32e0c2154cb7aa455734edf1c41021607 7.3 -b96177b0522706277881ddc3f1fce41077eb9993 7.4 -76fab4bb56d10e83f2af64ce7a7e39d56e88eb70 7.5 -a96c4d592397e79e2fa16b632fcc89b48ed63d04 7.6 -06d6fdfddc0a69a56d4b378816d7e5b59ae4a018 7.6.1 -cf0bb158f8d370f5195b95c917201f51aa53c0a9 7.6.2 -cfc54169af8f0fdc98fff6f06b98d325a81dfaeb 7.7 -baa58fde6e3b8820bbd77d5721760ec3f8f08716 7.7.1 -936d47e8d38319be37b06f0e05b1356eeaf7390e 8.0 -06219e4168be20471a7ceedf617f7464c0e63f6b 8.1 -095ad9f1f5db19d1dfc69f67d66e63b38f50270b 8.1.1 -66874c317d1692313127a0c7310e19f64fdc7bd0 8.1.2 -487d9a027220c215b1ddd04823aa5289ebc1120a 8.2 -5f1e61425fa995d3ac71b620a267f3d3a3447f25 8.2.1 From eb9e59fe57e909b34682535aa250fc40f6c3c017 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 11:45:04 -0500 Subject: [PATCH 033/835] Also note that docs aren't hosted with PyPI --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index b0bf1a76..1e2d755e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,7 @@ - Merge with latest skeleton, adding badges and test runs by default under tox instead of pytest-runner. +- Documentation is no longer hosted with PyPI. 8.2.1 ----- From 58bd3a497b1a79ce2ff77ab284fcfb24bd055387 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 11:47:55 -0500 Subject: [PATCH 034/835] Drop support for Python 2.6 and 3.2 --- .travis.yml | 2 -- CHANGES.rst | 6 ++++++ setup.py | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 10bf9258..86be7bc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ sudo: false language: python python: -- 2.6 - 2.7 -- 3.2 - 3.3 - 3.4 - 3.5 diff --git a/CHANGES.rst b/CHANGES.rst index 1e2d755e..a131352f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +9.0 +--- + +- Drop support for Python 2.6 and 3.2 as integration + dependencies (pip) no longer support these versions. + 8.3 --- diff --git a/setup.py b/setup.py index 2d327b4a..5b7fc897 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with io.open('README.rst', encoding='utf-8') as readme: long_description = readme.read() -needs_wheel = set(['release', 'bdist_wheel', 'dists']).intersection(sys.argv) +needs_wheel = {'release', 'bdist_wheel', 'dists'}.intersection(sys.argv) wheel = ['wheel'] if needs_wheel else [] name = 'path.py' @@ -40,9 +40,11 @@ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", ], entry_points={ From 756918e589c7c10eb123012e515a575cd0980d9b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 12:08:43 -0500 Subject: [PATCH 035/835] Add support for PyPy3 in tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 86be7bc7..88f96831 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - 3.3 - 3.4 - 3.5 +- pypy3 install: - pip install tox "setuptools>=28.2" script: From 1d7c5981c80b5411fef379b7436d4e0bda21cc24 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 12:58:12 -0500 Subject: [PATCH 036/835] Nevermind. pypy3 as found on Travis doens't have pip --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 88f96831..86be7bc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ python: - 3.3 - 3.4 - 3.5 -- pypy3 install: - pip install tox "setuptools>=28.2" script: From 786d75c682bfcbc88850d15fe3abc7c0758be447 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2016 13:06:22 -0500 Subject: [PATCH 037/835] Link to documentation. Fixes #118. --- README.rst | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index a9ef2d51..077117ca 100644 --- a/README.rst +++ b/README.rst @@ -16,16 +16,6 @@ License is indicated in the project metadata (typically one or more of the Trove classifiers). For more details, see `this explanation `_. -Docs -==== - -There's `no good mechanism for publishing documentation -`_ -easily. If there's a documentation link above, it's probably -stale because PyPI-based documentation is deprecated. This -project may have documentation published at ReadTheDocs, but -probably not. Good luck finding it. - path.py ======= @@ -42,6 +32,8 @@ example: ``path.py`` is `hosted at Github `_. +Find `the documentatation here `_. + Guides and Testimonials ======================= From a50fb1c894f7985bb71edf8d0ce60ef4f350b745 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 15 Dec 2016 14:40:27 -0500 Subject: [PATCH 038/835] Skip upload docs as it's deprecated anyway --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0c33b1ce..13ebb8aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,4 @@ deploy: user: jaraco # supply password with `travis encrypt --add deploy.password` distributions: dists + skip_upload_docs: true From 7e0d584b15bb297d1c0a6de59887b389e7cf87f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Dec 2016 15:36:52 -0500 Subject: [PATCH 039/835] Draft of an implementation accepting an implied name for a symlink based on the target's name. --- path.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/path.py b/path.py index fd45823f..cf2d7e0c 100644 --- a/path.py +++ b/path.py @@ -1308,11 +1308,16 @@ def link(self, newpath): return self._next_class(newpath) if hasattr(os, 'symlink'): - def symlink(self, newlink): + def symlink(self, newlink=None): """ Create a symbolic link at `newlink`, pointing here. + If newlink is not supplied, the symbolic link will assume + the name self.basename(), creating the link in the cwd. + .. seealso:: :func:`os.symlink` """ + if newlink is None: + newlink = self.basename() os.symlink(self, newlink) return self._next_class(newlink) From 99ffa27f0e7bd2eae63c84a0ded567eba4a2394b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:25:01 -0500 Subject: [PATCH 040/835] Remove rant about docs. If there's no link to the docs, then this is the docs. --- README.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.rst b/README.rst index 5196a108..0e0e26ee 100644 --- a/README.rst +++ b/README.rst @@ -15,13 +15,3 @@ License License is indicated in the project metadata (typically one or more of the Trove classifiers). For more details, see `this explanation `_. - -Docs -==== - -There's `no good mechanism for publishing documentation -`_ -easily. If there's a documentation link above, it's probably -stale because PyPI-based documentation is deprecated. This -project may have documentation published at ReadTheDocs, but -probably not. Good luck finding it. From 6245d0966d8dfb0fa2893c8a3e7d760c31d134d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:28:03 -0500 Subject: [PATCH 041/835] Prefer get_distribution --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index aa34defc..46d614b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ copyright = '2016 Jason R. Coombs' # The short X.Y version. -version = pkg_resources.require(project)[0].version +version = pkg_resources.get_distribution(project).version # The full version, including alpha/beta/rc tags. release = version From 3da8cf4a6f14abf5da05c9d46f3362dcc43d71a4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:42:35 -0500 Subject: [PATCH 042/835] No longer rely on the package being installed to retrieve the version. Instead, load the project name and version by invoking the setup script. --- docs/conf.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 46d614b9..adc9df77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import pkg_resources +import os +import sys +import subprocess extensions = [ 'sphinx.ext.autodoc', @@ -9,11 +11,15 @@ ] # General information about the project. -project = 'skeleton' + +root = os.path.join(os.path.dirname(__file__), '..') +setup_script = os.path.join(root, 'setup.py') +dist_info_cmd = [sys.executable, setup_script, '--name', '--version'] +output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) +project, version = output_bytes.decode('utf-8').split() + copyright = '2016 Jason R. Coombs' -# The short X.Y version. -version = pkg_resources.get_distribution(project).version # The full version, including alpha/beta/rc tags. release = version From fbadf0344d4b9ac6917e8546b5529c20082f4733 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:44:55 -0500 Subject: [PATCH 043/835] Also get the URL from the project metadata --- docs/conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index adc9df77..d52e40d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,9 +14,9 @@ root = os.path.join(os.path.dirname(__file__), '..') setup_script = os.path.join(root, 'setup.py') -dist_info_cmd = [sys.executable, setup_script, '--name', '--version'] +dist_info_cmd = [sys.executable, setup_script, '--name', '--version', '--url'] output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) -project, version = output_bytes.decode('utf-8').split() +project, version, url = output_bytes.decode('utf-8').split() copyright = '2016 Jason R. Coombs' @@ -30,11 +30,12 @@ using=dict( GH='https://github.com', project=project, + url=url, ), replace=[ dict( pattern=r"(Issue )?#(?P\d+)", - url='{GH}/jaraco/{project}/issues/{issue}', + url='{url}/issues/{issue}', ), dict( pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", From b2c592d84bc5f7c16a70b6d593fb320c5559eeee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:47:03 -0500 Subject: [PATCH 044/835] Also grab the author from the package metadata --- docs/conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d52e40d3..23a24476 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,11 +14,12 @@ root = os.path.join(os.path.dirname(__file__), '..') setup_script = os.path.join(root, 'setup.py') -dist_info_cmd = [sys.executable, setup_script, '--name', '--version', '--url'] +fields = ['--name', '--version', '--url', '--author'] +dist_info_cmd = [sys.executable, setup_script] + fields output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) -project, version, url = output_bytes.decode('utf-8').split() +project, version, url, author = output_bytes.decode('utf-8').split() -copyright = '2016 Jason R. Coombs' +copyright = '2016 ' + author # The full version, including alpha/beta/rc tags. release = version From b1133de832c3960777b9db80c070885c4bedd7c4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2016 08:57:20 -0500 Subject: [PATCH 045/835] Strip the trailing newline and then split on newline. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 23a24476..7402f72f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ fields = ['--name', '--version', '--url', '--author'] dist_info_cmd = [sys.executable, setup_script] + fields output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) -project, version, url, author = output_bytes.decode('utf-8').split() +project, version, url, author = output_bytes.decode('utf-8').strip().split('\n') copyright = '2016 ' + author From 203528be6dd662ba7ac2392473740e9dd3d0240b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 12:01:17 -0500 Subject: [PATCH 046/835] Move listdir optimizations into FastPath. Ref #116. --- path.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/path.py b/path.py index 31707f00..aafe6c1c 100644 --- a/path.py +++ b/path.py @@ -220,7 +220,7 @@ def _always_unicode(cls, path): Ensure the path as retrieved from a Python API, such as :func:`os.listdir`, is a proper Unicode string. """ - if isinstance(path, text_type): + if PY3 or isinstance(path, text_type): return path return path.decode(sys.getfilesystemencoding(), 'surrogateescape') @@ -529,17 +529,10 @@ def listdir(self, pattern=None): .. seealso:: :meth:`files`, :meth:`dirs` """ - children = os.listdir(self) - if not PY3: - children = map(self._always_unicode, children) - - if pattern is None: - return [self / child for child in children] - pattern, normcase = self._prepare_fnmatch_pattern(pattern) return [ self / child - for child in children + for child in map(self._always_unicode, os.listdir(self)) if self._next_class(child)._prepared_fnmatch(pattern, normcase) ] @@ -1775,6 +1768,30 @@ class CaseInsensitivePattern(text_type): def normcase(self): return __import__('ntpath').normcase + +class FastPath(Path): + """ + Performance optimized version of SimplePath for use + on embedded platforms and other systems with limited + CPU. See #115 and #116 for background. + """ + + def listdir(self, pattern=None): + children = os.listdir(self) + if not PY3: + children = map(self._always_unicode, children) + + if pattern is None: + return [self / child for child in children] + + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + return [ + self / child + for child in map(self._always_unicode, children) + if self._next_class(child)._prepared_fnmatch(pattern, normcase) + ] + + ######################## # Backward-compatibility class path(Path): From 42360e0458f4f3eb34878c9b1e72a963a33408a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 12:25:05 -0500 Subject: [PATCH 047/835] Move 'prepared' and 're-used' functionality, as added in b1a2461, to a FastPath subclass, encapsulating the optimizations and retaining the more simple implementation in the base Path class. Ref #116. --- path.py | 253 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 180 insertions(+), 73 deletions(-) diff --git a/path.py b/path.py index aafe6c1c..7bb819b1 100644 --- a/path.py +++ b/path.py @@ -529,11 +529,12 @@ def listdir(self, pattern=None): .. seealso:: :meth:`files`, :meth:`dirs` """ - pattern, normcase = self._prepare_fnmatch_pattern(pattern) + if pattern is None: + pattern = '*' return [ self / child for child in map(self._always_unicode, os.listdir(self)) - if self._next_class(child)._prepared_fnmatch(pattern, normcase) + if self._next_class(child).fnmatch(pattern) ] def dirs(self, pattern=None): @@ -592,15 +593,6 @@ def ignore(msg): raise ValueError("invalid errors parameter") errors = vars(Handlers).get(errors, errors) - if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) - else: - normcase = None - - return self._prepared_walk(pattern, normcase, errors) - - def _prepared_walk(self, pattern, normcase, errors): - """ Prepared version of walk """ try: childList = self.listdir() except Exception: @@ -611,7 +603,7 @@ def _prepared_walk(self, pattern, normcase, errors): return for child in childList: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.fnmatch(pattern): yield child try: isdir = child.isdir() @@ -623,7 +615,7 @@ def _prepared_walk(self, pattern, normcase, errors): isdir = False if isdir: - for item in child._prepared_walk(pattern, normcase, errors): + for item in child.walk(pattern, errors): yield item def walkdirs(self, pattern=None, errors='strict'): @@ -642,15 +634,6 @@ def walkdirs(self, pattern=None, errors='strict'): if errors not in ('strict', 'warn', 'ignore'): raise ValueError("invalid errors parameter") - if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) - else: - normcase = None - - return self._prepared_walkdirs(pattern, normcase, errors) - - def _prepared_walkdirs(self, pattern, normcase, errors): - """ Prepared version of walkdirs """ try: dirs = self.dirs() except Exception: @@ -666,9 +649,9 @@ def _prepared_walkdirs(self, pattern, normcase, errors): raise for child in dirs: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.fnmatch(pattern): yield child - for subsubdir in child._prepared_walkdirs(pattern, normcase, errors): + for subsubdir in child.walkdirs(pattern, errors): yield subsubdir def walkfiles(self, pattern=None, errors='strict'): @@ -682,15 +665,6 @@ def walkfiles(self, pattern=None, errors='strict'): if errors not in ('strict', 'warn', 'ignore'): raise ValueError("invalid errors parameter") - if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) - else: - normcase = None - - return self._prepared_walkfiles(pattern, normcase, errors) - - def _prepared_walkfiles(self, pattern, normcase, errors): - """ Prepared version of walkfiles """ try: childList = self.listdir() except Exception: @@ -722,45 +696,12 @@ def _prepared_walkfiles(self, pattern, normcase, errors): raise if isfile: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.fnmatch(pattern): yield child elif isdir: - for f in child._prepared_walkfiles(pattern, normcase, errors): + for f in child.walkfiles(pattern, errors): yield f - def _prepared_fnmatch(self, pattern, normcase): - """ Return ``True`` if `self.name` matches the given `pattern`, - prepared version. - - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. The pattern is expected to be normcase'd - already. - - `normcase` - A function used to normalize the pattern and - filename before matching. - - .. seealso:: :func:`Path.fnmatch` - """ - return fnmatch.fnmatchcase(normcase(self.name), pattern) - - def _prepare_fnmatch_pattern(self, pattern, normcase=None): - """ Prepares a fmatch_pattern for use with ``Path._prepared_fnmatch`. - - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. If the pattern contains a `normcase` - attribute, it is applied to the name and path prior to comparison. - - `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, which defaults - to :meth:`os.path.normcase`. - - .. seealso:: :func:`Path._prepared_fnmatch` - """ - if not normcase: - normcase = getattr(pattern, 'normcase', self.module.normcase) - pattern = normcase(pattern) - return pattern, normcase - def fnmatch(self, pattern, normcase=None): """ Return ``True`` if `self.name` matches the given `pattern`. @@ -774,11 +715,11 @@ def fnmatch(self, pattern, normcase=None): .. seealso:: :func:`fnmatch.fnmatch` """ - if not pattern: - raise ValueError("No pattern provided") - - pattern, normcase = self._prepare_fnmatch_pattern(pattern, normcase) - return self._prepared_fnmatch(pattern, normcase) + default_normcase = getattr(pattern, 'normcase', self.module.normcase) + normcase = normcase or default_normcase + name = normcase(self.name) + pattern = normcase(pattern) + return fnmatch.fnmatchcase(name, pattern) def glob(self, pattern): """ Return a list of Path objects that match the pattern. @@ -1791,6 +1732,172 @@ def listdir(self, pattern=None): if self._next_class(child)._prepared_fnmatch(pattern, normcase) ] + def walk(self, pattern=None, errors='strict'): + class Handlers: + def strict(msg): + raise + + def warn(msg): + warnings.warn(msg, TreeWalkWarning) + + def ignore(msg): + pass + + if not callable(errors) and errors not in vars(Handlers): + raise ValueError("invalid errors parameter") + errors = vars(Handlers).get(errors, errors) + + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walk(pattern, normcase, errors) + + def _prepared_walk(self, pattern, normcase, errors): + """ Prepared version of walk """ + try: + childList = self.listdir() + except Exception: + exc = sys.exc_info()[1] + tmpl = "Unable to list directory '%(self)s': %(exc)s" + msg = tmpl % locals() + errors(msg) + return + + for child in childList: + if pattern is None or child._prepared_fnmatch(pattern, normcase): + yield child + try: + isdir = child.isdir() + except Exception: + exc = sys.exc_info()[1] + tmpl = "Unable to access '%(child)s': %(exc)s" + msg = tmpl % locals() + errors(msg) + isdir = False + + if isdir: + for item in child._prepared_walk(pattern, normcase, errors): + yield item + + def walkdirs(self, pattern=None, errors='strict'): + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walkdirs(pattern, normcase, errors) + + def _prepared_walkdirs(self, pattern, normcase, errors): + """ Prepared version of walkdirs """ + try: + dirs = self.dirs() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in dirs: + if pattern is None or child._prepared_fnmatch(pattern, normcase): + yield child + for subsubdir in child._prepared_walkdirs(pattern, normcase, errors): + yield subsubdir + + def walkfiles(self, pattern=None, errors='strict'): + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + if pattern: + pattern, normcase = self._prepare_fnmatch_pattern(pattern) + else: + normcase = None + + return self._prepared_walkfiles(pattern, normcase, errors) + + def _prepared_walkfiles(self, pattern, normcase, errors): + """ Prepared version of walkfiles """ + try: + childList = self.listdir() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in childList: + try: + isfile = child.isfile() + isdir = not isfile and child.isdir() + except: + if errors == 'ignore': + continue + elif errors == 'warn': + warnings.warn( + "Unable to access '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + continue + else: + raise + + if isfile: + if pattern is None or child._prepared_fnmatch(pattern, normcase): + yield child + elif isdir: + for f in child._prepared_walkfiles(pattern, normcase, errors): + yield f + + def _prepared_fnmatch(self, pattern, normcase): + """ Return ``True`` if `self.name` matches the given `pattern`, + prepared version. + `pattern` - A filename pattern with wildcards, + for example ``'*.py'``. The pattern is expected to be normcase'd + already. + `normcase` - A function used to normalize the pattern and + filename before matching. + .. seealso:: :func:`Path.fnmatch` + """ + return fnmatch.fnmatchcase(normcase(self.name), pattern) + + def _prepare_fnmatch_pattern(self, pattern, normcase=None): + """ Prepares a fmatch_pattern for use with ``Path._prepared_fnmatch`. + `pattern` - A filename pattern with wildcards, + for example ``'*.py'``. If the pattern contains a `normcase` + attribute, it is applied to the name and path prior to comparison. + `normcase` - (optional) A function used to normalize the pattern and + filename before matching. Defaults to :meth:`self.module`, which defaults + to :meth:`os.path.normcase`. + .. seealso:: :func:`Path._prepared_fnmatch` + """ + if not normcase: + normcase = getattr(pattern, 'normcase', self.module.normcase) + pattern = normcase(pattern) + return pattern, normcase + + def fnmatch(self, pattern, normcase=None): + if not pattern: + raise ValueError("No pattern provided") + + pattern, normcase = self._prepare_fnmatch_pattern(pattern, normcase) + return self._prepared_fnmatch(pattern, normcase) + ######################## # Backward-compatibility From 2aaefa8d55edb51c9b48e152d642fcf110265bf8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 12:32:50 -0500 Subject: [PATCH 048/835] Rely on name-mangling and class hierarchy for namespacing. Ref #116. --- path.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/path.py b/path.py index 7bb819b1..b0d4b5ee 100644 --- a/path.py +++ b/path.py @@ -1725,11 +1725,11 @@ def listdir(self, pattern=None): if pattern is None: return [self / child for child in children] - pattern, normcase = self._prepare_fnmatch_pattern(pattern) + pattern, normcase = self.__prepare(pattern) return [ self / child for child in map(self._always_unicode, children) - if self._next_class(child)._prepared_fnmatch(pattern, normcase) + if self._next_class(child).__fnmatch(pattern, normcase) ] def walk(self, pattern=None, errors='strict'): @@ -1748,13 +1748,13 @@ def ignore(msg): errors = vars(Handlers).get(errors, errors) if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) + pattern, normcase = self.__prepare(pattern) else: normcase = None - return self._prepared_walk(pattern, normcase, errors) + return self.__walk(pattern, normcase, errors) - def _prepared_walk(self, pattern, normcase, errors): + def __walk(self, pattern, normcase, errors): """ Prepared version of walk """ try: childList = self.listdir() @@ -1766,7 +1766,7 @@ def _prepared_walk(self, pattern, normcase, errors): return for child in childList: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.__fnmatch(pattern, normcase): yield child try: isdir = child.isdir() @@ -1778,7 +1778,7 @@ def _prepared_walk(self, pattern, normcase, errors): isdir = False if isdir: - for item in child._prepared_walk(pattern, normcase, errors): + for item in child.__walk(pattern, normcase, errors): yield item def walkdirs(self, pattern=None, errors='strict'): @@ -1786,13 +1786,13 @@ def walkdirs(self, pattern=None, errors='strict'): raise ValueError("invalid errors parameter") if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) + pattern, normcase = self.__prepare(pattern) else: normcase = None - return self._prepared_walkdirs(pattern, normcase, errors) + return self.__walkdirs(pattern, normcase, errors) - def _prepared_walkdirs(self, pattern, normcase, errors): + def __walkdirs(self, pattern, normcase, errors): """ Prepared version of walkdirs """ try: dirs = self.dirs() @@ -1809,9 +1809,9 @@ def _prepared_walkdirs(self, pattern, normcase, errors): raise for child in dirs: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.__fnmatch(pattern, normcase): yield child - for subsubdir in child._prepared_walkdirs(pattern, normcase, errors): + for subsubdir in child.__walkdirs(pattern, normcase, errors): yield subsubdir def walkfiles(self, pattern=None, errors='strict'): @@ -1819,13 +1819,13 @@ def walkfiles(self, pattern=None, errors='strict'): raise ValueError("invalid errors parameter") if pattern: - pattern, normcase = self._prepare_fnmatch_pattern(pattern) + pattern, normcase = self.__prepare(pattern) else: normcase = None - return self._prepared_walkfiles(pattern, normcase, errors) + return self.__walkfiles(pattern, normcase, errors) - def _prepared_walkfiles(self, pattern, normcase, errors): + def __walkfiles(self, pattern, normcase, errors): """ Prepared version of walkfiles """ try: childList = self.listdir() @@ -1858,13 +1858,13 @@ def _prepared_walkfiles(self, pattern, normcase, errors): raise if isfile: - if pattern is None or child._prepared_fnmatch(pattern, normcase): + if pattern is None or child.__fnmatch(pattern, normcase): yield child elif isdir: - for f in child._prepared_walkfiles(pattern, normcase, errors): + for f in child.__walkfiles(pattern, normcase, errors): yield f - def _prepared_fnmatch(self, pattern, normcase): + def __fnmatch(self, pattern, normcase): """ Return ``True`` if `self.name` matches the given `pattern`, prepared version. `pattern` - A filename pattern with wildcards, @@ -1876,15 +1876,15 @@ def _prepared_fnmatch(self, pattern, normcase): """ return fnmatch.fnmatchcase(normcase(self.name), pattern) - def _prepare_fnmatch_pattern(self, pattern, normcase=None): - """ Prepares a fmatch_pattern for use with ``Path._prepared_fnmatch`. + def __prepare(self, pattern, normcase=None): + """ Prepares a fmatch_pattern for use with ``FastPath.__fnmatch`. `pattern` - A filename pattern with wildcards, for example ``'*.py'``. If the pattern contains a `normcase` attribute, it is applied to the name and path prior to comparison. `normcase` - (optional) A function used to normalize the pattern and filename before matching. Defaults to :meth:`self.module`, which defaults to :meth:`os.path.normcase`. - .. seealso:: :func:`Path._prepared_fnmatch` + .. seealso:: :func:`FastPath.__fnmatch` """ if not normcase: normcase = getattr(pattern, 'normcase', self.module.normcase) @@ -1895,8 +1895,8 @@ def fnmatch(self, pattern, normcase=None): if not pattern: raise ValueError("No pattern provided") - pattern, normcase = self._prepare_fnmatch_pattern(pattern, normcase) - return self._prepared_fnmatch(pattern, normcase) + pattern, normcase = self.__prepare(pattern, normcase) + return self.__fnmatch(pattern, normcase) ######################## From b108eeffe7afd1994f1e1cde52ec96d672f3b6a3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 13:01:38 -0500 Subject: [PATCH 049/835] Use a little bit of fixture magic, and tweak the tests slightly, so now all the tests run against both Path and FastPath. Ref #116. --- test_path.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/test_path.py b/test_path.py index f6aa1b67..27987658 100644 --- a/test_path.py +++ b/test_path.py @@ -30,17 +30,28 @@ import pytest -from path import Path, tempdir +import path +from path import tempdir from path import CaseInsensitivePattern as ci from path import SpecialResolver from path import Multi +Path = None + def p(**choices): """ Choose a value from several possible values, based on os.name """ return choices[os.name] +@pytest.fixture(autouse=True, params=[path.Path, path.FastPath]) +def path_class(request, monkeypatch): + """ + Invoke tests on any number of Path classes. + """ + monkeypatch.setitem(globals(), 'Path', request.param) + + class TestBasics: def test_relpath(self): root = Path(p(nt='C:\\', posix='/')) @@ -781,17 +792,17 @@ def test_chdir_or_cd(self, tmpdir): class TestSubclass: - class PathSubclass(Path): - pass def test_subclass_produces_same_class(self): """ When operations are invoked on a subclass, they should produce another instance of that subclass. """ - p = self.PathSubclass('/foo') + class PathSubclass(Path): + pass + p = PathSubclass('/foo') subdir = p / 'bar' - assert isinstance(subdir, self.PathSubclass) + assert isinstance(subdir, PathSubclass) class TestTempDir: @@ -801,7 +812,7 @@ def test_constructor(self): One should be able to readily construct a temporary directory """ d = tempdir() - assert isinstance(d, Path) + assert isinstance(d, path.Path) assert d.exists() assert d.isdir() d.rmdir() @@ -814,7 +825,7 @@ def test_next_class(self): """ d = tempdir() sub = d / 'subdir' - assert isinstance(sub, Path) + assert isinstance(sub, path.Path) d.rmdir() def test_context_manager(self): @@ -1075,7 +1086,8 @@ def test_for_class(self): cls = Multi.for_class(Path) assert issubclass(cls, Path) assert issubclass(cls, Multi) - assert cls.__name__ == 'MultiPath' + expected_name = 'Multi' + Path.__name__ + assert cls.__name__ == expected_name def test_detect_no_pathsep(self): """ From e446402f0008e7b0b224745b861aae6de6d2e5f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 13:25:14 -0500 Subject: [PATCH 050/835] Restore optimization, accidentally missed in refactor. Ref #116. --- path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path.py b/path.py index aafe6c1c..c4e2eeaf 100644 --- a/path.py +++ b/path.py @@ -1787,7 +1787,7 @@ def listdir(self, pattern=None): pattern, normcase = self._prepare_fnmatch_pattern(pattern) return [ self / child - for child in map(self._always_unicode, children) + for child in children if self._next_class(child)._prepared_fnmatch(pattern, normcase) ] From de58c0ccdfc22253e8b0c70bed3c3d585d661190 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 13:52:37 -0500 Subject: [PATCH 051/835] Remove _always_unicode. Fixes #121. --- CHANGES.rst | 9 +++++++++ path.py | 12 +----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a131352f..32d5d11e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +9.1 +--- + +- #121: Removed workaround for #61 added in 5.2. ``path.py`` + now only supports file system paths that can be effectively + decoded to text. It is the responsibility of the system + implementer to ensure that filenames on the system are + decodeable by ``sys.getfilesystemencoding()``. + 9.0 --- diff --git a/path.py b/path.py index fd45823f..1504b1b1 100644 --- a/path.py +++ b/path.py @@ -216,16 +216,6 @@ def _next_class(cls): """ return cls - @classmethod - def _always_unicode(cls, path): - """ - Ensure the path as retrieved from a Python API, such as :func:`os.listdir`, - is a proper Unicode string. - """ - if PY3 or isinstance(path, text_type): - return path - return path.decode(sys.getfilesystemencoding(), 'surrogateescape') - # --- Special Python methods. def __repr__(self): @@ -535,7 +525,7 @@ def listdir(self, pattern=None): pattern = '*' return [ self / child - for child in map(self._always_unicode, os.listdir(self)) + for child in os.listdir(self) if self._next_class(child).fnmatch(pattern) ] From 55f94572a846566068e3b666e34ddd489149b2e0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 15:29:35 -0500 Subject: [PATCH 052/835] Complete the merge --- path.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/path.py b/path.py index ccf95d9b..1f648d4d 100644 --- a/path.py +++ b/path.py @@ -1711,9 +1711,6 @@ class FastPath(Path): def listdir(self, pattern=None): children = os.listdir(self) - if not PY3: - children = map(self._always_unicode, children) - if pattern is None: return [self / child for child in children] From e3e34e8ab244bf11ba4ded6537c5050df0588ede Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 15:40:29 -0500 Subject: [PATCH 053/835] Mark test as xfail where it's no longer supported. Ref #121 --- test_path.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_path.py b/test_path.py index f6aa1b67..d0a06ec3 100644 --- a/test_path.py +++ b/test_path.py @@ -30,6 +30,7 @@ import pytest +import path from path import Path, tempdir from path import CaseInsensitivePattern as ci from path import SpecialResolver @@ -351,6 +352,8 @@ def test_listing(self, tmpdir): except: pass + @pytest.mark.xfail(platform.system() == 'Linux' and path.PY2, + reason="Can't decode bytes in FS. See #121") def test_listdir_other_encoding(self, tmpdir): """ Some filesystems allow non-character sequences in path names. From 84b53d90527040c58c4236698c95cb6cd1f2736d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 15:54:14 -0500 Subject: [PATCH 054/835] Default upload URL is now in Python 3.6. Use that. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4659acce..e0803242 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,3 @@ dists = clean --all sdist bdist_wheel [wheel] universal = 1 - -[upload] -repository = https://upload.pypi.org/legacy/ From 65802c2bdef667991476f52d0f1eed8042abdf58 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 16:14:44 -0500 Subject: [PATCH 055/835] Reindent to match PEP-8 --- path.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/path.py b/path.py index 1504b1b1..c2901cf8 100644 --- a/path.py +++ b/path.py @@ -50,7 +50,7 @@ import re import contextlib import io -from distutils import dir_util +import distutils.dir_util import importlib import itertools @@ -1382,8 +1382,12 @@ def merge_tree(self, dst, symlinks=False, *args, **kwargs): self.copytree(stage, symlinks, *args, **kwargs) # now copy everything from the stage directory using # the semantics of dir_util.copy_tree - dir_util.copy_tree(stage, dst, preserve_symlinks=symlinks, - update=update) + distutils.dir_util.copy_tree( + stage, + dst, + preserve_symlinks=symlinks, + update=update, + ) # # --- Special stuff from os From 393fcca4c6feabe4f997c2e30208aa1a40173bea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 16:26:18 -0500 Subject: [PATCH 056/835] Add issue references to 8.1.x releases. --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 32d5d11e..c24e754d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -43,12 +43,12 @@ 8.1.1 ----- -Reluctantly restored reference to path.path in ``__all__``. +- #102: Reluctantly restored reference to path.path in ``__all__``. 8.1 --- -Restored ``path.path`` with a DeprecationWarning. +- #102: Restored ``path.path`` with a DeprecationWarning. 8.0 --- From a781071bf0fc9ff477b2d141baec343a7b3ab0e9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2017 16:36:05 -0500 Subject: [PATCH 057/835] Remove path.path compatibility shim. --- CHANGES.rst | 5 +++++ path.py | 12 ------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c24e754d..f200068e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +10.0 +---- + +- Once again as in 8.0 remove deprecated ``path.path``. + 9.1 --- diff --git a/path.py b/path.py index c2901cf8..9527f96c 100644 --- a/path.py +++ b/path.py @@ -1704,15 +1704,3 @@ class CaseInsensitivePattern(text_type): @property def normcase(self): return __import__('ntpath').normcase - -######################## -# Backward-compatibility -class path(Path): - def __new__(cls, *args, **kwargs): - msg = "path is deprecated. Use Path instead." - warnings.warn(msg, DeprecationWarning) - return Path.__new__(cls, *args, **kwargs) - - -__all__ += ['path'] -######################## From 1d9d5b3aaaea7d5eca780e6847a59c5907569cab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 3 Jan 2017 16:39:27 -0500 Subject: [PATCH 058/835] Don't fail if XDG_CONFIG_HOME is set for the test runner. Fixes #122. --- test_path.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test_path.py b/test_path.py index d0a06ec3..7533ae18 100644 --- a/test_path.py +++ b/test_path.py @@ -1026,6 +1026,7 @@ def test_unix_paths(self, tmpdir, monkeypatch, feign_linux): def test_unix_paths_fallback(self, tmpdir, monkeypatch, feign_linux): "Without XDG_CONFIG_HOME set, ~/.config should be used." fake_home = tmpdir / '_home' + monkeypatch.delitem(os.environ, 'XDG_CONFIG_HOME', raising=False) monkeypatch.setitem(os.environ, 'HOME', str(fake_home)) expected = str(tmpdir / '_home' / '.config') assert SpecialResolver(Path).user.config == expected From 5853c7e7e738bc641f95835f239b04d8c7a853e3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 5 Jan 2017 09:31:05 -0500 Subject: [PATCH 059/835] setup is already present in the module name. Just call them params. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 83a22f68..45243679 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ name = 'skeleton' description = '' -setup_params = dict( +params = dict( name=name, use_scm_version=True, author="Jason R. Coombs", @@ -45,4 +45,4 @@ }, ) if __name__ == '__main__': - setuptools.setup(**setup_params) + setuptools.setup(**params) From 746dd7999f9db23276144ee2160920bd01ed860c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 17 Jan 2017 19:56:28 -0500 Subject: [PATCH 060/835] Use Python 3.6 by default --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13ebb8aa..91ba39a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ sudo: false language: python python: - 2.7 -- 3.5 +- 3.6 install: - pip install tox "setuptools>=28.2" script: @@ -16,7 +16,7 @@ deploy: on: tags: true all_branches: true - python: 3.5 + python: 3.6 user: jaraco # supply password with `travis encrypt --add deploy.password` distributions: dists From 9f6eea591eaae483be11d13ebad06958a6a1e2c8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Jan 2017 11:13:08 -0500 Subject: [PATCH 061/835] No longer rely on setup_requires for wheel. --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 45243679..157ecb2e 100644 --- a/setup.py +++ b/setup.py @@ -3,16 +3,12 @@ # Project skeleton maintained at https://github.com/jaraco/skeleton import io -import sys import setuptools with io.open('README.rst', encoding='utf-8') as readme: long_description = readme.read() -needs_wheel = {'release', 'bdist_wheel', 'dists'}.intersection(sys.argv) -wheel = ['wheel'] if needs_wheel else [] - name = 'skeleton' description = '' @@ -33,7 +29,7 @@ }, setup_requires=[ 'setuptools_scm>=1.15.0', - ] + wheel, + ], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", From 21a4e86e6fef7fba92afb4bcb49d79859d0cb2b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 23 Jan 2017 07:10:36 -0500 Subject: [PATCH 062/835] Add PEP substitution in changelog. --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 7402f72f..8639b2cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,10 @@ pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", ), + dict( + pattern=r"PEP[- ](?P\d+)", + url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', + ), ], ), } From 8c85974469d05d39bec4ab4cf3aab65de5a40a4c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 23 Jan 2017 07:11:49 -0500 Subject: [PATCH 063/835] Add __fspath__. Fixes #123. --- CHANGES.rst | 5 +++++ docs/conf.py | 42 +++++++++++++++++++++--------------------- path.py | 3 +++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f200068e..69a2423d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +10.1 +---- + +- #123: Implement ``Path.__fspath__`` per PEP 519. + 10.0 ---- diff --git a/docs/conf.py b/docs/conf.py index 8e378f62..9335f0b7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,25 +38,25 @@ intersphinx_mapping = {'python': ('http://docs.python.org/', None)} link_files = { - '../CHANGES.rst': dict( - using=dict( - GH='https://github.com', - project=project, - url=url, - ), - replace=[ - dict( - pattern=r"(Issue )?#(?P\d+)", - url='{url}/issues/{issue}', - ), - dict( - pattern=r"Pull Request ?#(?P\d+)", - url='{GH}/jaraco/{project}/pull/{pull_request}', - ), - dict( - pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", - with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", - ), - ], - ), + '../CHANGES.rst': dict( + using=dict( + GH='https://github.com', + project=project, + url=url, + ), + replace=[ + dict( + pattern=r"(Issue )?#(?P\d+)", + url='{url}/issues/{issue}', + ), + dict( + pattern=r"Pull Request ?#(?P\d+)", + url='{GH}/jaraco/{project}/pull/{pull_request}', + ), + dict( + pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", + with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", + ), + ], + ), } diff --git a/path.py b/path.py index 9527f96c..d18ec06b 100644 --- a/path.py +++ b/path.py @@ -269,6 +269,9 @@ def __enter__(self): def __exit__(self, *_): os.chdir(self._old_dir) + def __fspath__(self): + return self + @classmethod def getcwd(cls): """ Return the current working directory as a path object. From d9d2c5e83aaa9bedecc9cf4b1231642fb0e29fef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 23 Jan 2017 07:18:31 -0500 Subject: [PATCH 064/835] Remove Python 2.6 requirement --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 33b226ba..78be6115 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,6 @@ install_requires=[ ], extras_require={ - ':python_version=="2.6"': ['importlib'], }, setup_requires=[ 'setuptools_scm>=1.15.0', From ee0d8647d8537b9de2aeafeba4acd74910f98a4f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Jan 2017 20:59:15 -0500 Subject: [PATCH 065/835] Add support for Python 2.6 in docs conf --- docs/conf.py | 3 +++ tests/requirements.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 8639b2cc..7402c7a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,6 +5,9 @@ import sys import subprocess +if 'check_output' not in dir(subprocess): + import subprocess32 as subprocess + extensions = [ 'sphinx.ext.autodoc', 'rst.linker', diff --git a/tests/requirements.txt b/tests/requirements.txt index 70bc02f1..ab484054 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1 +1,2 @@ pytest >= 2.8 +subprocess32; python_version=="2.6" From e690b031cc3c07b657dc235252a284d8023a38dc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 9 Feb 2017 12:29:49 -0500 Subject: [PATCH 066/835] Set the origin date once and forget it. --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 7402c7a1..bf6ae64f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ import os import sys import subprocess +import datetime if 'check_output' not in dir(subprocess): import subprocess32 as subprocess @@ -22,7 +23,10 @@ output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) project, version, url, author = output_bytes.decode('utf-8').strip().split('\n') -copyright = '2016 ' + author +origin_date = datetime.date(2017,1,1) +today = datetime.date.today() + +copyright = '{origin_date.year}-{today.year} {author}'.format(**locals()) # The full version, including alpha/beta/rc tags. release = version From b728c5892b394392044b245cba43f46740efb851 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Mar 2017 13:33:19 -0400 Subject: [PATCH 067/835] Add python_requires directive. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 157ecb2e..892b6b35 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ packages=setuptools.find_packages(), include_package_data=True, namespace_packages=name.split('.')[:-1], + python_requires='>=2.7', install_requires=[ ], extras_require={ From 59c37d70f1140bf18b9a48398cc4502ebce91b5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Mar 2017 14:36:54 -0400 Subject: [PATCH 068/835] Don't bother with copyright year(s). Let the repository history track the changes and copyright years. YAGNI. --- docs/conf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bf6ae64f..fc947971 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,10 +23,7 @@ output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) project, version, url, author = output_bytes.decode('utf-8').strip().split('\n') -origin_date = datetime.date(2017,1,1) -today = datetime.date.today() - -copyright = '{origin_date.year}-{today.year} {author}'.format(**locals()) +copyright = author # The full version, including alpha/beta/rc tags. release = version From 049284c6c440217c4f686b61c0980b5e0100626b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Mar 2017 14:39:02 -0400 Subject: [PATCH 069/835] Include the project (for docstrings). Include Sphinx (for environments where it's not an implied provision). --- docs/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 442df9fa..c11e7555 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ +. +sphinx rst.linker From b9bcd869482ea0ff636c8848896a94e24a6fbfca Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Mar 2017 14:40:02 -0400 Subject: [PATCH 070/835] Include pytest-sugar for nicer test output. --- tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index ab484054..6d65b375 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,3 @@ pytest >= 2.8 +pytest-sugar subprocess32; python_version=="2.6" From 908cf4ad0e27813933ada7cc9f16ebce9ac0c6cc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 30 Mar 2017 04:36:53 -0400 Subject: [PATCH 071/835] Rely on jaraco.packaging for loading the package metadata from the package for Sphinx. --- docs/conf.py | 27 ++------------------------- docs/requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fc947971..0e11c827 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,46 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import os -import sys -import subprocess -import datetime - -if 'check_output' not in dir(subprocess): - import subprocess32 as subprocess - extensions = [ 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', 'rst.linker', ] -# General information about the project. - -root = os.path.join(os.path.dirname(__file__), '..') -setup_script = os.path.join(root, 'setup.py') -fields = ['--name', '--version', '--url', '--author'] -dist_info_cmd = [sys.executable, setup_script] + fields -output_bytes = subprocess.check_output(dist_info_cmd, cwd=root) -project, version, url, author = output_bytes.decode('utf-8').strip().split('\n') - -copyright = author - -# The full version, including alpha/beta/rc tags. -release = version - master_doc = 'index' link_files = { '../CHANGES.rst': dict( using=dict( GH='https://github.com', - project=project, - url=url, ), replace=[ dict( pattern=r"(Issue )?#(?P\d+)", - url='{url}/issues/{issue}', + url='{package_url}/issues/{issue}', ), dict( pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", diff --git a/docs/requirements.txt b/docs/requirements.txt index c11e7555..e7b6a745 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ . sphinx -rst.linker +jaraco.packaging>=3.2 +rst.linker>=1.9 From 689f700fcfcdfcdc7d027f204a9654b101ac9ecb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 30 Mar 2017 04:43:46 -0400 Subject: [PATCH 072/835] Use single-quotes to satisfy the style nazis. --- docs/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0e11c827..8bc82981 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,15 +16,15 @@ ), replace=[ dict( - pattern=r"(Issue )?#(?P\d+)", + pattern=r'(Issue )?#(?P\d+)', url='{package_url}/issues/{issue}', ), dict( - pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", - with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", + pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( - pattern=r"PEP[- ](?P\d+)", + pattern=r'PEP[- ](?P\d+)', url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), ], From 23dae906a2563a2da30e0d67490fa009576f6439 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 30 Mar 2017 20:41:18 -0400 Subject: [PATCH 073/835] The requirement is no longer needed for tests. --- tests/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 6d65b375..d9e0f332 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,2 @@ pytest >= 2.8 pytest-sugar -subprocess32; python_version=="2.6" From eb8a5fab573b26e6aa3d1b46cf8b157478b135d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 09:07:07 -0500 Subject: [PATCH 074/835] Add appveyor for testing on Windows --- appveyor.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..97bb9b85 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,19 @@ +environment: + + APPVEYOR: true + + matrix: + - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python27-x64" + +install: + # symlink python from a directory with a space + - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" + - "SET PYTHON=\"C:\\Program Files\\Python\"" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + +build: off + +test_script: + - "python -m pip install tox" + - "tox" From cfc0e4f181e3f035cd8d2f847524417efc808b0c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 09:18:06 -0500 Subject: [PATCH 075/835] Appdirs hard-codes the forward slash for ~/.config, so use that as the expectation. Fixes #125. --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index 7533ae18..40189b79 100644 --- a/test_path.py +++ b/test_path.py @@ -1028,7 +1028,7 @@ def test_unix_paths_fallback(self, tmpdir, monkeypatch, feign_linux): fake_home = tmpdir / '_home' monkeypatch.delitem(os.environ, 'XDG_CONFIG_HOME', raising=False) monkeypatch.setitem(os.environ, 'HOME', str(fake_home)) - expected = str(tmpdir / '_home' / '.config') + expected = Path('~/.config').expanduser() assert SpecialResolver(Path).user.config == expected def test_property(self): From 0d535e811cf078d2295d496c3c84e5614e91fa1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 10:13:41 -0500 Subject: [PATCH 076/835] Extract method for checking the link --- test_path.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test_path.py b/test_path.py index 40189b79..ad98abc9 100644 --- a/test_path.py +++ b/test_path.py @@ -704,6 +704,9 @@ def testing_structure(self, tmpdir): else: self.test_file.copy(self.test_link) + def check_link(self): + assert Path(self.subdir_b / self.test_link.name).islink() + def test_with_nonexisting_dst_kwargs(self): self.subdir_a.merge_tree(self.subdir_b, symlinks=True) assert self.subdir_b.isdir() @@ -712,7 +715,7 @@ def test_with_nonexisting_dst_kwargs(self): self.subdir_b / self.test_link.name, )) assert set(self.subdir_b.listdir()) == expected - assert Path(self.subdir_b / self.test_link.name).islink() + self.check_link() def test_with_nonexisting_dst_args(self): self.subdir_a.merge_tree(self.subdir_b, True) @@ -722,7 +725,7 @@ def test_with_nonexisting_dst_args(self): self.subdir_b / self.test_link.name, )) assert set(self.subdir_b.listdir()) == expected - assert Path(self.subdir_b / self.test_link.name).islink() + self.check_link() def test_with_existing_dst(self): self.subdir_b.rmtree() @@ -743,7 +746,7 @@ def test_with_existing_dst(self): self.subdir_b / test_new.name, )) assert set(self.subdir_b.listdir()) == expected - assert Path(self.subdir_b / self.test_link.name).islink() + self.check_link() assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 def test_copytree_parameters(self): From 106005d223fddfb6369b3a0b6679fc7f4c4d4c1a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 10:19:15 -0500 Subject: [PATCH 077/835] Change the assertion based on symlink availability. Fixes #126. --- test_path.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index ad98abc9..9ed62012 100644 --- a/test_path.py +++ b/test_path.py @@ -705,7 +705,9 @@ def testing_structure(self, tmpdir): self.test_file.copy(self.test_link) def check_link(self): - assert Path(self.subdir_b / self.test_link.name).islink() + target = Path(self.subdir_b / self.test_link.name) + check = target.islink if hasattr(os, 'symlink') else target.isfile + assert check() def test_with_nonexisting_dst_kwargs(self): self.subdir_a.merge_tree(self.subdir_b, symlinks=True) From d620fb1164ac5561f6dca50978014cad83b77b03 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 10:32:30 -0500 Subject: [PATCH 078/835] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a131352f..7e972e77 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +10.2 +---- + +- Symlink no longer requires the ``newlink`` parameter + and will default to the basename of the target in the + current working directory. + 9.0 --- From fc0471dc065c985d83e7aea5aa27d601a04b1e4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 10:42:53 -0500 Subject: [PATCH 079/835] Update comment to reference the canonical Path. --- path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path.py b/path.py index f53683c4..d424e7ec 100644 --- a/path.py +++ b/path.py @@ -1716,7 +1716,7 @@ def normcase(self): class FastPath(Path): """ - Performance optimized version of SimplePath for use + Performance optimized version of Path for use on embedded platforms and other systems with limited CPU. See #115 and #116 for background. """ From 4b9e000a0544c8baf2b168cfc6344ac5d2b068c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Apr 2017 10:52:09 -0500 Subject: [PATCH 080/835] Update changelog --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3a997fb6..de48120b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +10.3 +---- + +- #115: Added a new performance-optimized implementation + for listdir operations, optimizing ``listdir``, ``walk``, + ``walkfiles``, ``walkdirs``, and ``fnmatch``, presented + as the ``FastPath`` class. + + Please direct feedback on this implementation to the ticket, + especially if the performance benefits justify it replacing + the default ``Path`` class. + 10.2 ---- From 73aa3be250e64e3a94c68b53df0b288f4e931151 Mon Sep 17 00:00:00 2001 From: dassh Date: Mon, 17 Apr 2017 16:45:42 +0800 Subject: [PATCH 081/835] Add test for rmdir_p --- test_path.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test_path.py b/test_path.py index 8e76ed6f..449fbf94 100644 --- a/test_path.py +++ b/test_path.py @@ -693,6 +693,18 @@ def test_rmtree_p(self, tmpdir): self.fail("Calling `rmtree_p` on non-existent directory " "should not raise an exception.") + def test_rmdir_p(self, tmpdir): + d = Path(tmpdir) + sub = d / 'subfolder' + sub.mkdir() + sub.rmdir_p() + assert not sub.exists() + try: + sub.rmdir_p() + except OSError: + raise Exception("Calling `rmdir_p` on non-existent directory " + "should not raise an exception.") + class TestMergeTree: @pytest.fixture(autouse=True) From 131cc04834571f663cab36c873ab7fb052bbf4d3 Mon Sep 17 00:00:00 2001 From: dassh Date: Mon, 17 Apr 2017 16:46:53 +0800 Subject: [PATCH 082/835] Fixed 'rmdir_p' raise error when directory does not exist --- path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path.py b/path.py index d424e7ec..052c5c61 100644 --- a/path.py +++ b/path.py @@ -1231,7 +1231,7 @@ def rmdir_p(self): self.rmdir() except OSError: _, e, _ = sys.exc_info() - if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: + if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST and e.errno != errno.ENOENT: raise return self From 79e7a3583234ca2104f5ce6d20c3b8f7a808531f Mon Sep 17 00:00:00 2001 From: dassh Date: Mon, 17 Apr 2017 16:47:10 +0800 Subject: [PATCH 083/835] Update changelog --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index de48120b..fcb44df9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,7 @@ +0.0.1 +---- +- #124: Fixed ``rmdir_p`` raise ``FileNotFoundError `` when directory does not exist. + 10.3 ---- From 1066d783ad9aca14bd3127a83c6f8fc548898dd3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2017 21:49:26 -0500 Subject: [PATCH 084/835] Edit changelog for consistency and correctness. --- CHANGES.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fcb44df9..130e8b5c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,8 @@ -0.0.1 ----- -- #124: Fixed ``rmdir_p`` raise ``FileNotFoundError `` when directory does not exist. +10.3.1 +------ + +- #124: Fixed ``rmdir_p`` raising ``FileNotFoundError`` when + directory does not exist on Windows. 10.3 ---- From 29f62cffec1e630fec1cd962377636bb90a1a17e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2017 21:52:40 -0500 Subject: [PATCH 085/835] Extract bypass codes for brevity --- path.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/path.py b/path.py index 052c5c61..22ed9e07 100644 --- a/path.py +++ b/path.py @@ -1231,7 +1231,8 @@ def rmdir_p(self): self.rmdir() except OSError: _, e, _ = sys.exc_info() - if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST and e.errno != errno.ENOENT: + bypass_codes = errno.ENOTEMPTY, errno.EEXIST, errno.ENOENT + if e.errno not in bypass_codes: raise return self From 368b3fd6b23dc7b68dc2720aca17a27a07567a04 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2017 21:57:20 -0500 Subject: [PATCH 086/835] Separate test of rmdir_p to existing and missing target --- test_path.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test_path.py b/test_path.py index 449fbf94..e524c207 100644 --- a/test_path.py +++ b/test_path.py @@ -693,17 +693,25 @@ def test_rmtree_p(self, tmpdir): self.fail("Calling `rmtree_p` on non-existent directory " "should not raise an exception.") - def test_rmdir_p(self, tmpdir): + def test_rmdir_p_exists(self, tmpdir): + """ + Invocation of rmdir_p on an existant directory should + remove the directory. + """ d = Path(tmpdir) sub = d / 'subfolder' sub.mkdir() sub.rmdir_p() assert not sub.exists() - try: - sub.rmdir_p() - except OSError: - raise Exception("Calling `rmdir_p` on non-existent directory " - "should not raise an exception.") + + def test_rmdir_p_nonexistent(self, tmpdir): + """ + A non-existent file should not raise an exception. + """ + d = Path(tmpdir) + sub = d / 'subfolder' + assert not sub.exists() + sub.rmdir_p() class TestMergeTree: From fbe7cb7fa1c0f4e30f6ac6e886c49ab6491aa959 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2017 12:21:45 -0400 Subject: [PATCH 087/835] Add readthedocs yml file --- .readthedocs.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..e83d731b --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,3 @@ +requirements_file: docs/requirements.txt +python: + version: 3 From 243e44fdb797ae54a08eb02d924f88e775e74ba9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2017 12:31:54 -0400 Subject: [PATCH 088/835] Move requirements for docs and testing into extras --- .readthedocs.yml | 4 +++- docs/requirements.txt | 4 ---- setup.py | 9 +++++++++ tests/requirements.txt | 2 -- tox.ini | 4 +--- 5 files changed, 13 insertions(+), 10 deletions(-) delete mode 100644 docs/requirements.txt delete mode 100644 tests/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml index e83d731b..8ae44684 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,3 +1,5 @@ -requirements_file: docs/requirements.txt python: version: 3 + extra_requirements: + - docs + pip_install: true diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index e7b6a745..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -. -sphinx -jaraco.packaging>=3.2 -rst.linker>=1.9 diff --git a/setup.py b/setup.py index 892b6b35..2bed3f7c 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,15 @@ install_requires=[ ], extras_require={ + 'testing': [ + 'pytest>=2.8', + 'pytest-sugar', + ], + 'docs': [ + 'sphinx', + 'jaraco.packaging>=3.2', + 'rst.linker>=1.9', + ], }, setup_requires=[ 'setuptools_scm>=1.15.0', diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index d9e0f332..00000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest >= 2.8 -pytest-sugar diff --git a/tox.ini b/tox.ini index d740130c..8efcba6f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,4 @@ [testenv] -deps = - -rtests/requirements.txt - commands = py.test {posargs} usedevelop = True +extras = testing From 3383a3aceb435cef929c13dff3e54e46af01cf49 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 26 Apr 2017 10:29:35 -0400 Subject: [PATCH 089/835] Add appveyor script for CI testing on Windows. --- appveyor.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..e856af3b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,19 @@ +environment: + + APPVEYOR: true + + matrix: + - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python27-x64" + +install: + # symlink python from a directory with a space + - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" + - "SET PYTHON=\"C:\\Program Files\\Python\"" + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + +build: off + +test_script: + - "python -m pip install tox" + - "tox" From 110cb56c59e99c5d0c630612f8593a7ef55ce732 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Apr 2017 19:21:34 -0400 Subject: [PATCH 090/835] Require tox 2.4 or later; fixes #2. --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 8efcba6f..1ae06efe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,6 @@ +[tox] +minversion = 2.4 + [testenv] commands = py.test {posargs} usedevelop = True From c84284022a198d560e685c5a687458a5be4c5fe6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 May 2017 21:42:53 -0400 Subject: [PATCH 091/835] Remove namespace_packages declaration, no longer needed. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 2bed3f7c..a7aeae12 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ url="https://github.com/jaraco/" + name, packages=setuptools.find_packages(), include_package_data=True, - namespace_packages=name.split('.')[:-1], python_requires='>=2.7', install_requires=[ ], From 31fb9d19ee7751ced7a0339268b2cd7b3ceb4701 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Jun 2017 14:02:51 -0400 Subject: [PATCH 092/835] Use a simple build number rather than prefixing with '1.0.' --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index e856af3b..0a8ce5c5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,3 +17,5 @@ build: off test_script: - "python -m pip install tox" - "tox" + +version: '{build}' From 39a3073ec8ec49ffdf2ad58cf03c7159f7031806 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Jul 2017 15:23:48 -0400 Subject: [PATCH 093/835] Feed the hobgoblins (delint). --- path.py | 65 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/path.py b/path.py index 22ed9e07..a139be76 100644 --- a/path.py +++ b/path.py @@ -78,6 +78,7 @@ text_type = str getcwdu = os.getcwd + def surrogate_escape(error): """ Simulate the Python 3 ``surrogateescape`` handler, but for Python 2 only. @@ -88,6 +89,7 @@ def surrogate_escape(error): val += 0xdc00 return __builtin__.unichr(val), error.end + if PY2: import __builtin__ string_types = __builtin__.basestring, @@ -96,6 +98,7 @@ def surrogate_escape(error): codecs.register_error('surrogateescape', surrogate_escape) map = itertools.imap + @contextlib.contextmanager def io_error_compat(): try: @@ -109,6 +112,7 @@ def io_error_compat(): ############################################################################## + __all__ = ['Path', 'CaseInsensitivePattern'] @@ -432,8 +436,9 @@ def uncshare(self): @multimethod def joinpath(cls, first, *others): """ - Join first to zero or more :class:`Path` components, adding a separator - character (:samp:`{first}.module.sep`) if needed. Returns a new instance of + Join first to zero or more :class:`Path` components, + adding a separator character (:samp:`{first}.module.sep`) + if needed. Returns a new instance of :samp:`{first}._next_class`. .. seealso:: :func:`os.path.join` @@ -705,8 +710,8 @@ def fnmatch(self, pattern, normcase=None): attribute, it is applied to the name and path prior to comparison. `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, which defaults - to :meth:`os.path.normcase`. + filename before matching. Defaults to :meth:`self.module`, which + defaults to :meth:`os.path.normcase`. .. seealso:: :func:`fnmatch.fnmatch` """ @@ -947,8 +952,8 @@ def read_md5(self): def _hash(self, hash_name): """ Returns a hash object for the file at the current path. - `hash_name` should be a hash algo name (such as ``'md5'`` or ``'sha1'``) - that's available in the :mod:`hashlib` module. + `hash_name` should be a hash algo name (such as ``'md5'`` + or ``'sha1'``) that's available in the :mod:`hashlib` module. """ m = hashlib.new(hash_name) for chunk in self.chunks(8192, mode="rb"): @@ -1171,7 +1176,8 @@ def chown(self, uid=-1, gid=-1): gid = grp.getgrnam(gid).gr_gid os.chown(self, uid, gid) else: - raise NotImplementedError("Ownership not available on this platform.") + msg = "Ownership not available on this platform." + raise NotImplementedError(msg) return self def rename(self, new): @@ -1415,19 +1421,23 @@ def startfile(self): # in-place re-writing, courtesy of Martijn Pieters # http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ @contextlib.contextmanager - def in_place(self, mode='r', buffering=-1, encoding=None, errors=None, - newline=None, backup_extension=None): + def in_place( + self, mode='r', buffering=-1, encoding=None, errors=None, + newline=None, backup_extension=None, + ): """ - A context in which a file may be re-written in-place with new content. + A context in which a file may be re-written in-place with + new content. - Yields a tuple of :samp:`({readable}, {writable})` file objects, where `writable` - replaces `readable`. + Yields a tuple of :samp:`({readable}, {writable})` file + objects, where `writable` replaces `readable`. If an exception occurs, the old file is restored, removing the written data. - Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only read-only-modes are - allowed. A :exc:`ValueError` is raised on invalid modes. + Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only + read-only-modes are allowed. A :exc:`ValueError` is raised + on invalid modes. For example, to add line numbers to a file:: @@ -1453,22 +1463,28 @@ def in_place(self, mode='r', buffering=-1, encoding=None, errors=None, except os.error: pass os.rename(self, backup_fn) - readable = io.open(backup_fn, mode, buffering=buffering, - encoding=encoding, errors=errors, newline=newline) + readable = io.open( + backup_fn, mode, buffering=buffering, + encoding=encoding, errors=errors, newline=newline, + ) try: perm = os.fstat(readable.fileno()).st_mode except OSError: - writable = open(self, 'w' + mode.replace('r', ''), + writable = open( + self, 'w' + mode.replace('r', ''), buffering=buffering, encoding=encoding, errors=errors, - newline=newline) + newline=newline, + ) else: os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC if hasattr(os, 'O_BINARY'): os_mode |= os.O_BINARY fd = os.open(self, os_mode, perm) - writable = io.open(fd, "w" + mode.replace('r', ''), + writable = io.open( + fd, "w" + mode.replace('r', ''), buffering=buffering, encoding=encoding, errors=errors, - newline=newline) + newline=newline, + ) try: if hasattr(os, 'chmod'): os.chmod(self, perm) @@ -1591,8 +1607,9 @@ def _next_class(cls): class tempdir(Path): """ - A temporary directory via :func:`tempfile.mkdtemp`, and constructed with the - same parameters that you can use as a context manager. + A temporary directory via :func:`tempfile.mkdtemp`, and + constructed with the same parameters that you can use + as a context manager. Example: @@ -1884,8 +1901,8 @@ def __prepare(self, pattern, normcase=None): for example ``'*.py'``. If the pattern contains a `normcase` attribute, it is applied to the name and path prior to comparison. `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, which defaults - to :meth:`os.path.normcase`. + filename before matching. Defaults to :meth:`self.module`, + which defaults to :meth:`os.path.normcase`. .. seealso:: :func:`FastPath.__fnmatch` """ if not normcase: From aa54cbe61d50068f00f4e5be847570c28ea10780 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Jul 2017 15:30:23 -0400 Subject: [PATCH 094/835] Don't lint this function, which doesn't have the issue in its original form. --- path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path.py b/path.py index a139be76..eb2d0e52 100644 --- a/path.py +++ b/path.py @@ -137,7 +137,7 @@ class TreeWalkWarning(Warning): # from jaraco.functools def compose(*funcs): - compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) + compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa return functools.reduce(compose_two, funcs) From 16d68a9fd19e6a726cc86fd1e3ec5f2d24788345 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Aug 2017 08:14:57 -0400 Subject: [PATCH 095/835] Restore support for namespace package declaration, selected on a 'nspkg_technique' setting --- setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setup.py b/setup.py index a7aeae12..72d901c4 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,11 @@ name = 'skeleton' description = '' +nspkg_technique = 'native' +""" +Does this package use "native" namespace packages or +pkg_resources "managed" namespace packages? +""" params = dict( name=name, @@ -22,6 +27,10 @@ url="https://github.com/jaraco/" + name, packages=setuptools.find_packages(), include_package_data=True, + namespace_packages=( + name.split('.')[:-1] if nspkg_technique == 'managed' + else [] + ), python_requires='>=2.7', install_requires=[ ], From 250cb960021233160e78a6f2c2780cfc1c964b9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Aug 2017 15:35:09 -0400 Subject: [PATCH 096/835] Inspired by pypa/setuptools#1059, use the preferred bdist_wheel heading. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e0803242..b0c90cbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,5 +2,5 @@ release = dists upload dists = clean --all sdist bdist_wheel -[wheel] +[bdist_wheel] universal = 1 From 6683cadb58a7b8e8a197d0c458157c80d4e4cb5c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Aug 2017 08:51:45 -0400 Subject: [PATCH 097/835] Remove surrogate_escape handler, unused. Ref #130. --- CHANGES.rst | 6 ++++++ path.py | 13 ------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 130e8b5c..24476582 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +10.4 +---- + +- #130: Removed surrogate_escape handler as it's no longer + used. + 10.3.1 ------ diff --git a/path.py b/path.py index eb2d0e52..766ebb42 100644 --- a/path.py +++ b/path.py @@ -41,7 +41,6 @@ import fnmatch import glob import shutil -import codecs import hashlib import errno import tempfile @@ -79,23 +78,11 @@ getcwdu = os.getcwd -def surrogate_escape(error): - """ - Simulate the Python 3 ``surrogateescape`` handler, but for Python 2 only. - """ - chars = error.object[error.start:error.end] - assert len(chars) == 1 - val = ord(chars) - val += 0xdc00 - return __builtin__.unichr(val), error.end - - if PY2: import __builtin__ string_types = __builtin__.basestring, text_type = __builtin__.unicode getcwdu = os.getcwdu - codecs.register_error('surrogateescape', surrogate_escape) map = itertools.imap From 7cd63e76c5add486c12271cfc613ced1d449b621 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Aug 2017 08:58:20 -0400 Subject: [PATCH 098/835] Feed the hobgoblins (delint). --- test_path.py | 132 ++++++++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 60 deletions(-) diff --git a/test_path.py b/test_path.py index e524c207..959cfb46 100644 --- a/test_path.py +++ b/test_path.py @@ -62,14 +62,14 @@ def test_relpath(self): up = Path(os.pardir) # basics - assert root.relpathto(boz) == Path('foo')/'bar'/'Baz'/'Boz' - assert bar.relpathto(boz) == Path('Baz')/'Boz' - assert quux.relpathto(boz) == up/'bar'/'Baz'/'Boz' - assert boz.relpathto(quux) == up/up/up/'quux' - assert boz.relpathto(bar) == up/up + assert root.relpathto(boz) == Path('foo') / 'bar' / 'Baz' / 'Boz' + assert bar.relpathto(boz) == Path('Baz') / 'Boz' + assert quux.relpathto(boz) == up / 'bar' / 'Baz' / 'Boz' + assert boz.relpathto(quux) == up / up / up / 'quux' + assert boz.relpathto(bar) == up / up # Path is not the first element in concatenation - assert root.relpathto(boz) == 'foo'/Path('bar')/'Baz'/'Boz' + assert root.relpathto(boz) == 'foo' / Path('bar') / 'Baz' / 'Boz' # x.relpathto(x) == curdir assert root.relpathto(root) == os.curdir @@ -123,7 +123,7 @@ def test_string_compatibility(self): # Test p1/p1. p1 = Path("foo") p2 = Path("bar") - assert p1/p2 == p(nt='foo\\bar', posix='foo/bar') + assert p1 / p2 == p(nt='foo\\bar', posix='foo/bar') def test_properties(self): # Create sample path object. @@ -293,12 +293,12 @@ def test_touch(self, tmpdir): ct = f.ctime assert t0 <= ct <= t1 - time.sleep(threshold*2) + time.sleep(threshold * 2) fobj = open(f, 'ab') fobj.write('some bytes'.encode('utf-8')) fobj.close() - time.sleep(threshold*2) + time.sleep(threshold * 2) t2 = time.time() - threshold f.touch() t3 = time.time() + threshold @@ -362,8 +362,10 @@ def test_listing(self, tmpdir): except: pass - @pytest.mark.xfail(platform.system() == 'Linux' and path.PY2, - reason="Can't decode bytes in FS. See #121") + @pytest.mark.xfail( + platform.system() == 'Linux' and path.PY2, + reason="Can't decode bytes in FS. See #121", + ) def test_listdir_other_encoding(self, tmpdir): """ Some filesystems allow non-character sequences in path names. @@ -511,28 +513,28 @@ def assertList(self, listing, expected): def test_patterns(self, tmpdir): d = Path(tmpdir) names = ['x.tmp', 'x.xtmp', 'x2g', 'x22', 'x.txt'] - dirs = [d, d/'xdir', d/'xdir.tmp', d/'xdir.tmp'/'xsubdir'] + dirs = [d, d / 'xdir', d / 'xdir.tmp', d / 'xdir.tmp' / 'xsubdir'] for e in dirs: if not e.isdir(): e.makedirs() for name in names: - (e/name).touch() - self.assertList(d.listdir('*.tmp'), [d/'x.tmp', d/'xdir.tmp']) - self.assertList(d.files('*.tmp'), [d/'x.tmp']) - self.assertList(d.dirs('*.tmp'), [d/'xdir.tmp']) + (e / name).touch() + self.assertList(d.listdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) + self.assertList(d.files('*.tmp'), [d / 'x.tmp']) + self.assertList(d.dirs('*.tmp'), [d / 'xdir.tmp']) self.assertList(d.walk(), [e for e in dirs - if e != d] + [e/n for e in dirs + if e != d] + [e / n for e in dirs for n in names]) self.assertList(d.walk('*.tmp'), - [e/'x.tmp' for e in dirs] + [d/'xdir.tmp']) - self.assertList(d.walkfiles('*.tmp'), [e/'x.tmp' for e in dirs]) - self.assertList(d.walkdirs('*.tmp'), [d/'xdir.tmp']) + [e / 'x.tmp' for e in dirs] + [d / 'xdir.tmp']) + self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) + self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) def test_unicode(self, tmpdir): d = Path(tmpdir) - p = d/'unicode.txt' + p = d / 'unicode.txt' def test(enc): """ Test that path works with the specified encoding, @@ -540,18 +542,22 @@ def test(enc): Unicode codepoints. """ - given = ('Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\r\n' - '\u0d0a\u0a0d\u0d15\u0a15\x85' - '\u0d0a\u0a0d\u0d15\u0a15\u2028' - '\r' - 'hanging') - clean = ('Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\n' - 'hanging') + given = ( + 'Hello world\n' + '\u0d0a\u0a0d\u0d15\u0a15\r\n' + '\u0d0a\u0a0d\u0d15\u0a15\x85' + '\u0d0a\u0a0d\u0d15\u0a15\u2028' + '\r' + 'hanging' + ) + clean = ( + 'Hello world\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\n' + 'hanging' + ) givenLines = [ ('Hello world\n'), ('\u0d0a\u0a0d\u0d15\u0a15\r\n'), @@ -594,8 +600,9 @@ def test(enc): return # Write Unicode to file using path.write_text(). - cleanNoHanging = clean + '\n' # This test doesn't work with a - # hanging line. + # This test doesn't work with a hanging line. + cleanNoHanging = clean + '\n' + p.write_text(cleanNoHanging, enc) p.write_text(cleanNoHanging, enc, append=True) # Check the result. @@ -663,8 +670,10 @@ def test_chunks(self, tmpdir): assert i == len(txt) / size - 1 - @pytest.mark.skipif(not hasattr(os.path, 'samefile'), - reason="samefile not present") + @pytest.mark.skipif( + not hasattr(os.path, 'samefile'), + reason="samefile not present", + ) def test_samefile(self, tmpdir): f1 = (tempdir() / '1.txt').touch() f1.write_text('foo') @@ -703,7 +712,7 @@ def test_rmdir_p_exists(self, tmpdir): sub.mkdir() sub.rmdir_p() assert not sub.exists() - + def test_rmdir_p_nonexistent(self, tmpdir): """ A non-existent file should not raise an exception. @@ -712,7 +721,7 @@ def test_rmdir_p_nonexistent(self, tmpdir): sub = d / 'subfolder' assert not sub.exists() sub.rmdir_p() - + class TestMergeTree: @pytest.fixture(autouse=True) @@ -929,8 +938,8 @@ def test_listdir_empty_pattern(self): def test_listdir_patterns(self, tmpdir): p = Path(tmpdir) - (p/'sub').mkdir() - (p/'File').touch() + (p / 'sub').mkdir() + (p / 'File').touch() assert p.listdir('s*') == [p / 'sub'] assert len(p.listdir('*')) == 2 @@ -941,14 +950,14 @@ def test_listdir_custom_module(self, tmpdir): """ always_unix = Path.using_module(posixpath) p = always_unix(tmpdir) - (p/'sub').mkdir() - (p/'File').touch() + (p / 'sub').mkdir() + (p / 'File').touch() assert p.listdir('S*') == [] always_win = Path.using_module(ntpath) p = always_win(tmpdir) - assert p.listdir('S*') == [p/'sub'] - assert p.listdir('f*') == [p/'File'] + assert p.listdir('S*') == [p / 'sub'] + assert p.listdir('f*') == [p / 'File'] def test_listdir_case_insensitive(self, tmpdir): """ @@ -956,27 +965,30 @@ def test_listdir_case_insensitive(self, tmpdir): used by that Path class. """ p = Path(tmpdir) - (p/'sub').mkdir() - (p/'File').touch() - assert p.listdir(ci('S*')) == [p/'sub'] - assert p.listdir(ci('f*')) == [p/'File'] + (p / 'sub').mkdir() + (p / 'File').touch() + assert p.listdir(ci('S*')) == [p / 'sub'] + assert p.listdir(ci('f*')) == [p / 'File'] assert p.files(ci('S*')) == [] assert p.dirs(ci('f*')) == [] def test_walk_case_insensitive(self, tmpdir): p = Path(tmpdir) - (p/'sub1'/'foo').makedirs_p() - (p/'sub2'/'foo').makedirs_p() - (p/'sub1'/'foo'/'bar.Txt').touch() - (p/'sub2'/'foo'/'bar.TXT').touch() - (p/'sub2'/'foo'/'bar.txt.bz2').touch() + (p / 'sub1' / 'foo').makedirs_p() + (p / 'sub2' / 'foo').makedirs_p() + (p / 'sub1' / 'foo' / 'bar.Txt').touch() + (p / 'sub2' / 'foo' / 'bar.TXT').touch() + (p / 'sub2' / 'foo' / 'bar.txt.bz2').touch() files = list(p.walkfiles(ci('*.txt'))) assert len(files) == 2 - assert p/'sub2'/'foo'/'bar.TXT' in files - assert p/'sub1'/'foo'/'bar.Txt' in files + assert p / 'sub2' / 'foo' / 'bar.TXT' in files + assert p / 'sub1' / 'foo' / 'bar.Txt' in files + -@pytest.mark.skipif(sys.version_info < (2, 6), - reason="in_place requires io module in Python 2.6") +@pytest.mark.skipif( + sys.version_info < (2, 6), + reason="in_place requires io module in Python 2.6", +) class TestInPlace: reference_content = textwrap.dedent(""" The quick brown fox jumped over the lazy dog. @@ -997,7 +1009,7 @@ class TestInPlace: @classmethod def create_reference(cls, tmpdir): - p = Path(tmpdir)/'document' + p = Path(tmpdir) / 'document' with p.open('w') as stream: stream.write(cls.reference_content) return p @@ -1022,7 +1034,7 @@ def test_exception_in_context(self, tmpdir): assert "some error" in str(exc) with doc.open() as stream: data = stream.read() - assert not 'Lorem' in data + assert 'Lorem' not in data assert 'lazy dog' in data From 88d315ae9adab430bd36722da8c6ab74c2e79cf0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 13 Sep 2017 04:24:11 -0400 Subject: [PATCH 099/835] Check the docs during tests --- setup.py | 1 + tox.ini | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72d901c4..75f23712 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ 'testing': [ 'pytest>=2.8', 'pytest-sugar', + 'collective.checkdocs', ], 'docs': [ 'sphinx', diff --git a/tox.ini b/tox.ini index 1ae06efe..16bf78a7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,8 @@ minversion = 2.4 [testenv] -commands = py.test {posargs} +commands = + py.test {posargs} + python setup.py checkdocs usedevelop = True extras = testing From 835393c93e4fec867b5e2e0a638e7a14994c6b1d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2017 18:05:16 -0400 Subject: [PATCH 100/835] Use stages in travis to have deployment depend on success in all Python versions. --- .travis.yml | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91ba39a9..e2914e2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,32 @@ +dist: trusty sudo: false language: python -python: -- 2.7 -- 3.6 + +jobs: + fast_finish: true + include: + - python: 2.7 + - python: &latest_py3 3.6 + - stage: deploy + if: tag IS present + python: *latest_py3 + install: skip + script: skip + before_deploy: python bootstrap.py + deploy: + provider: pypi + on: + tags: true + all_branches: true + user: jaraco + # supply password with `travis encrypt --add deploy.password` + distributions: dists + skip_cleanup: true + skip_upload_docs: true + +cache: pip + install: -- pip install tox "setuptools>=28.2" -script: -- tox -branches: - except: - - skeleton -deploy: - provider: pypi - server: https://upload.pypi.org/legacy/ - on: - tags: true - all_branches: true - python: 3.6 - user: jaraco - # supply password with `travis encrypt --add deploy.password` - distributions: dists - skip_upload_docs: true +- pip install tox + +script: tox From a419524c69e7c2f8b9327ff2f6ec9f61e89c9c30 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2017 19:46:21 -0400 Subject: [PATCH 101/835] Remove 'bootstrap', artifact from setuptools --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e2914e2b..7932518e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ jobs: python: *latest_py3 install: skip script: skip - before_deploy: python bootstrap.py deploy: provider: pypi on: From a953d1baa6c6cd2b4b9e5f06378a706b3555259d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Sep 2017 16:36:17 -0400 Subject: [PATCH 102/835] --add doesn't work in a list --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7932518e..d4eecec2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,8 @@ jobs: tags: true all_branches: true user: jaraco - # supply password with `travis encrypt --add deploy.password` + password: + secure: ... # encrypt password with `travis encrypt` distributions: dists skip_cleanup: true skip_upload_docs: true From 5dc924c102a23d06cafd8e0850f0f35582cbd9aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 26 Sep 2017 12:02:30 +0100 Subject: [PATCH 103/835] Add a license file. Fixes jaraco/skeleton#1. --- LICENSE | 7 +++++++ README.rst | 8 -------- 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..5e795a61 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.rst b/README.rst index 0e0e26ee..1c5d10ec 100644 --- a/README.rst +++ b/README.rst @@ -7,11 +7,3 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: http://travis-ci.org/jaraco/skeleton - - -License -======= - -License is indicated in the project metadata (typically one or more -of the Trove classifiers). For more details, see `this explanation -`_. From d149ed4d5dc659c81d0567b227523d95ee8e04c3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 16 Oct 2017 12:13:14 -0400 Subject: [PATCH 104/835] Remove downloads shield, no longer available. --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 1c5d10ec..5161ae54 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,5 @@ .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg -.. image:: https://img.shields.io/pypi/dm/skeleton.svg - .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: http://travis-ci.org/jaraco/skeleton From 34958ccc94b8c473ebf8adcdc35b82b6023d8702 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 17 Oct 2017 19:47:02 -0400 Subject: [PATCH 105/835] Add documentation badge. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 5161ae54..bcdb31e5 100644 --- a/README.rst +++ b/README.rst @@ -5,3 +5,6 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: http://travis-ci.org/jaraco/skeleton + +.. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest + :target: http://skeleton.readthedocs.io/en/latest/?badge=latest From 6ce5de241b2fb5ef3a7e20e6b2b43da1ed506264 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Oct 2017 10:58:48 -0400 Subject: [PATCH 106/835] Add docs about Advantages and add references to Alternatives. --- README.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.rst b/README.rst index 1d65d1e0..8422059b 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,41 @@ The latest release is always updated to the `Python Package Index You may also always download the source distribution (zip/tarball), extract it, and run ``python setup.py`` to install it. +Advantages +========== + +Python 3.4 introduced +`pathlib `_, +which shares many characteristics with ``path.py``. In particular, +it provides an object encapsulation for representing filesystem paths. +One may have imagined ``pathlib`` would supersede ``path.py``. + +But the implementation and the usage quickly diverge, and ``path.py`` +has several advantages over ``pathlib``: + +- ``path.py`` implementsbits ``Path`` objects as a subclass of + ``str`` (unicode on Python 2), and as a result these ``Path`` + objects may be passed directly to other APIs that expect simple + text representations of paths, whereas with ``pathlib``, one + must first cast values to strings before passing them to + APIs unaware of pathlib. +- ``path.py`` goes beyond exposing basic functionality of a path + and exposes commonly-used behaviors on a path, providing + methods like ``rmtree`` (from shlib) and ``remove_p`` (remove + a file if it exists). +- As a PyPI-hosted package, ``path.py`` is free to iterate + more quickly than a stdlib package. Contributions are welcomed + and encouraged. + +Alternatives +============ + +In addition to +`pathlib `_, the +`pylib project `_ implements a +`LocalPath `_ +class, which shares some behaviors and interfaces with ``path.py``. + Development =========== From b982e7e7f11d56584806ecc768b4ba80f3643b83 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Oct 2017 10:59:23 -0400 Subject: [PATCH 107/835] Try to forget easy_install exists --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8422059b..64758124 100644 --- a/README.rst +++ b/README.rst @@ -84,7 +84,7 @@ Development To install an in-development version, use the Github links to clone or download a snapshot of the latest code. Alternatively, if you have git -installed, you may be able to use ``pip`` or ``easy_install`` to install directly from +installed, you may be able to use ``pip`` to install directly from the repository:: pip install git+https://github.com/jaraco/path.py.git From 2a9305197e55d521f9aa3bd652dbd1f4bb267bc6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Oct 2017 11:00:18 -0400 Subject: [PATCH 108/835] Correct SyntaxError in example --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 64758124..267f436c 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ example: from path import Path d = Path('/home/guido/bin') for f in d.files('*.py'): - f.chmod(0755) + f.chmod(0o755) ``path.py`` is `hosted at Github `_. From 01851a9712aafe312fbc6e0dc33a693ca7fa47f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Oct 2017 11:02:04 -0400 Subject: [PATCH 109/835] Fix test failure on checkdocs --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 7eb8f95a..c03b0cb7 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,9 @@ 'pytest-sugar', 'collective.checkdocs', 'appdirs', + + # required for checkdocs on README.rst + 'pygments', ], 'docs': [ 'sphinx', From 889893927fe3de50a6d782210aaaa607638208c3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Oct 2017 11:05:00 -0400 Subject: [PATCH 110/835] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 24476582..7f4a1e15 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +10.5 +---- + +- Packaging refresh and readme updates. + 10.4 ---- From 9f3070058624b7305b59ff4c7adeae2aeecef565 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Nov 2017 08:24:24 -0500 Subject: [PATCH 111/835] Feed the hobgoblins (delint). --- test_path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test_path.py b/test_path.py index 959cfb46..7fe79d13 100644 --- a/test_path.py +++ b/test_path.py @@ -359,7 +359,7 @@ def test_listing(self, tmpdir): for f in files: try: f.remove() - except: + except Exception: pass @pytest.mark.xfail( @@ -923,7 +923,8 @@ def test_fnmatch_custom_mod(self): assert p.fnmatch('FOO[ABC]AR') def test_fnmatch_custom_normcase(self): - normcase = lambda path: path.upper() + def normcase(path): + return path.upper() p = Path('FooBar') assert p.fnmatch('foobar', normcase=normcase) assert p.fnmatch('FOO[ABC]AR', normcase=normcase) From b550adcb40021faf65a6464523925278d4b0b1c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Nov 2017 08:39:25 -0500 Subject: [PATCH 112/835] Expect failure on macOS 10.13 and later. Ref #132. --- setup.py | 1 + test_path.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/setup.py b/setup.py index c03b0cb7..08c4e3bb 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ 'pytest-sugar', 'collective.checkdocs', 'appdirs', + 'packaging', # required for checkdocs on README.rst 'pygments', diff --git a/test_path.py b/test_path.py index 7fe79d13..018bb7ee 100644 --- a/test_path.py +++ b/test_path.py @@ -27,8 +27,10 @@ import textwrap import platform import importlib +import operator import pytest +import packaging.version import path from path import tempdir @@ -52,6 +54,18 @@ def path_class(request, monkeypatch): monkeypatch.setitem(globals(), 'Path', request.param) +def mac_version(target, comparator=operator.ge): + """ + Return True if on a Mac whose version passes the comparator. + """ + current_ver = packaging.version.parse(platform.mac_ver()[0]) + target_ver = packaging.version.parse(target) + return ( + platform.system() == 'Darwin' + and comparator(current_ver, target_ver) + ) + + class TestBasics: def test_relpath(self): root = Path(p(nt='C:\\', posix='/')) @@ -366,6 +380,10 @@ def test_listing(self, tmpdir): platform.system() == 'Linux' and path.PY2, reason="Can't decode bytes in FS. See #121", ) + @pytest.mark.xfail( + mac_version('10.13'), + reason="macOS disallows invalid encodings", + ) def test_listdir_other_encoding(self, tmpdir): """ Some filesystems allow non-character sequences in path names. From 60d4de2b44f5690d857cfd889edb1906c21b6bc8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Nov 2017 08:48:05 -0500 Subject: [PATCH 113/835] On macOS 10.13 and later, use an approximate test. Fixes #132. --- setup.py | 2 +- test_path.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 08c4e3bb..c4584edd 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ ], extras_require={ 'testing': [ - 'pytest>=2.8', + 'pytest>=3.0', 'pytest-sugar', 'collective.checkdocs', 'appdirs', diff --git a/test_path.py b/test_path.py index 018bb7ee..21dd3d87 100644 --- a/test_path.py +++ b/test_path.py @@ -329,6 +329,9 @@ def test_touch(self, tmpdir): # On Windows, "ctime" is CREATION time assert ct == ct2 assert ct2 < t2 + elif mac_version('10.13'): + # On macOS High Sierra, f.mtime will be close + assert ct2 == pytest.approx(f.mtime, 0.001) else: # On other systems, it might be the CHANGE time # (especially on Unix, time of inode changes) From 569fc4cf66799d410beab23d96eb75d706933be3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Nov 2017 08:56:11 -0500 Subject: [PATCH 114/835] Mark test as xfail on Windows on Python 3. Fixes #133. --- test_path.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_path.py b/test_path.py index 21dd3d87..74208708 100644 --- a/test_path.py +++ b/test_path.py @@ -387,6 +387,10 @@ def test_listing(self, tmpdir): mac_version('10.13'), reason="macOS disallows invalid encodings", ) + @pytest.mark.xfail( + platform.system() == 'Windows' and path.PY3, + reason="Can't write latin characters. See #133", + ) def test_listdir_other_encoding(self, tmpdir): """ Some filesystems allow non-character sequences in path names. From 6c36336e9fc45048ad43e4ff494c9d1ffc14fc49 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 13 Nov 2017 09:14:44 -0500 Subject: [PATCH 115/835] Normalize indentation in docs/conf.py --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8bc82981..14744ee8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,9 +2,9 @@ # -*- coding: utf-8 -*- extensions = [ - 'sphinx.ext.autodoc', - 'jaraco.packaging.sphinx', - 'rst.linker', + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', + 'rst.linker', ] master_doc = 'index' From 99622ab0e3d295a3ec17f69fb21dc68c94cc7fda Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Nov 2017 10:26:38 -0500 Subject: [PATCH 116/835] Declare 'python' factor at top level --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d4eecec2..c7ed90b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,12 @@ dist: trusty sudo: false language: python +python: +- 2.7 +- &latest_py3 3.6 + jobs: fast_finish: true - include: - - python: 2.7 - - python: &latest_py3 3.6 - stage: deploy if: tag IS present python: *latest_py3 From ce5e0eb6ef5943bfa15edf5f9ea3f74e71ab00e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Nov 2017 13:17:16 -0500 Subject: [PATCH 117/835] Correct travis syntax --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c7ed90b2..61180dba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: jobs: fast_finish: true + include: - stage: deploy if: tag IS present python: *latest_py3 From b4b4c1116886f7cb10729a2d42272e41618ca20f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Nov 2017 15:45:57 -0500 Subject: [PATCH 118/835] reference the license file in metadata --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index b0c90cbf..378a8e4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,6 @@ dists = clean --all sdist bdist_wheel [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE From dd475b7ad1784a564929c711781eebd9b85ba063 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Jan 2018 19:32:19 -0500 Subject: [PATCH 119/835] Use https --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index bcdb31e5..043561df 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg - :target: http://travis-ci.org/jaraco/skeleton + :target: https://travis-ci.org/jaraco/skeleton .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest - :target: http://skeleton.readthedocs.io/en/latest/?badge=latest + :target: https://skeleton.readthedocs.io/en/latest/?badge=latest From 97c492d425da509f96f554a66499d13723d147bf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Jan 2018 19:32:38 -0500 Subject: [PATCH 120/835] Add build-docs env in tox. --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tox.ini b/tox.ini index 16bf78a7..c3b6d2e6 100644 --- a/tox.ini +++ b/tox.ini @@ -7,3 +7,11 @@ commands = python setup.py checkdocs usedevelop = True extras = testing + +[testenv:build-docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx . {toxinidir}/build/html From 63659653697e9037ed5cb5a770dc00b07c77e5a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Jan 2018 15:34:05 -0500 Subject: [PATCH 121/835] Run only default environment by default. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index c3b6d2e6..7cccd421 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +envlist = python minversion = 2.4 [testenv] From 99d850f0ed9852993626d4869e9f096e1643be6d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 17 Jan 2018 09:55:12 -0500 Subject: [PATCH 122/835] To support namespace packages, Setuptools must be 31.0.1. This change is necessary with the adoption of tox-venv, which uses Python's venv, which does not install the latest setuptools by default. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 7cccd421..df1b0eff 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,8 @@ envlist = python minversion = 2.4 [testenv] +deps = + setuptools>=31.0.1 commands = py.test {posargs} python setup.py checkdocs From bbc018de3cbee4bccdcbb58b637c0c659e6e37e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jan 2018 11:59:01 -0500 Subject: [PATCH 123/835] Need to avoid .eggs in recursing dirs. Ref pypa/setuptools_scm#212. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 56a87745..1b2f6247 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] -norecursedirs=dist build .tox +norecursedirs=dist build .tox .eggs addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS From b9aba822c7e154d1ad185585fe7642947b3c5265 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Jan 2018 09:06:00 -0500 Subject: [PATCH 124/835] Use tox-venv for future compatibility. --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61180dba..85540ec0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,6 @@ jobs: cache: pip install: -- pip install tox +- pip install tox tox-venv script: tox diff --git a/appveyor.yml b/appveyor.yml index 0a8ce5c5..3d55a92b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,7 +15,7 @@ install: build: off test_script: - - "python -m pip install tox" + - "python -m pip install tox tox-venv" - "tox" version: '{build}' From 34ab781f8768ab5101f087cfffe7e38b94048a7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 1 Feb 2018 09:28:53 -0500 Subject: [PATCH 125/835] Disable pytest-sugar until Frozenball/pytest-sugar#133 is addressed. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 75f23712..e07ba771 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ extras_require={ 'testing': [ 'pytest>=2.8', - 'pytest-sugar', + # 'pytest-sugar', 'collective.checkdocs', ], 'docs': [ From 3902aabd7f5c4cb0f4aba8d2785da98e87cb7d6a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 Feb 2018 13:24:23 -0500 Subject: [PATCH 126/835] Bring back pytest-sugar with a minimum version to support Pytest 3.4. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e07ba771..9e73e231 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ extras_require={ 'testing': [ 'pytest>=2.8', - # 'pytest-sugar', + 'pytest-sugar>=0.9.1', 'collective.checkdocs', ], 'docs': [ From f922dacdba8a79a7e6ec0d0848c42a9c0fe69dcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Feb 2018 10:14:11 -0500 Subject: [PATCH 127/835] Delint --- path.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/path.py b/path.py index 766ebb42..75c3f918 100644 --- a/path.py +++ b/path.py @@ -670,7 +670,7 @@ def walkfiles(self, pattern=None, errors='strict'): try: isfile = child.isfile() isdir = not isfile and child.isdir() - except: + except Exception: if errors == 'ignore': continue elif errors == 'warn': @@ -918,14 +918,15 @@ def write_lines(self, lines, encoding=None, errors='strict', to read the file later. """ with self.open('ab' if append else 'wb') as f: - for l in lines: - isUnicode = isinstance(l, text_type) + for line in lines: + isUnicode = isinstance(line, text_type) if linesep is not None: pattern = U_NL_END if isUnicode else NL_END - l = pattern.sub('', l) + linesep + line = pattern.sub('', line) + linesep if isUnicode: - l = l.encode(encoding or sys.getdefaultencoding(), errors) - f.write(l) + line = line.encode( + encoding or sys.getdefaultencoding(), errors) + f.write(line) def read_md5(self): """ Calculate the md5 hash for this file. @@ -1635,7 +1636,8 @@ def _multi_permission_mask(mode): >>> _multi_permission_mask('a=r,u+w')(0) == 0o644 True """ - compose = lambda f, g: lambda *args, **kwargs: g(f(*args, **kwargs)) + def compose(f, g): + return lambda *args, **kwargs: g(f(*args, **kwargs)) return functools.reduce(compose, map(_permission_mask, mode.split(','))) @@ -1851,7 +1853,7 @@ def __walkfiles(self, pattern, normcase, errors): try: isfile = child.isfile() isdir = not isfile and child.isdir() - except: + except Exception: if errors == 'ignore': continue elif errors == 'warn': From 84caced30e4005cecc0cb31170d0d51526ddb3b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Feb 2018 10:56:30 -0500 Subject: [PATCH 128/835] Change namebase to stem to align with pathlib. --- CHANGES.rst | 6 ++++++ path.py | 13 ++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7f4a1e15..a9150c0d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +10.6 +---- + +- Renamed ``namebase`` to ``stem`` to match API of pathlib. + Kept ``namebase`` as a deprecated alias for compatibility. + 10.5 ---- diff --git a/path.py b/path.py index 75c3f918..fc3d6de6 100644 --- a/path.py +++ b/path.py @@ -316,17 +316,20 @@ def expand(self): return self.expandvars().expanduser().normpath() @property - def namebase(self): + def stem(self): """ The same as :meth:`name`, but with one file extension stripped off. - For example, - ``Path('/home/guido/python.tar.gz').name == 'python.tar.gz'``, - but - ``Path('/home/guido/python.tar.gz').namebase == 'python.tar'``. + >>> Path('/home/guido/python.tar.gz').stem + 'python.tar' """ base, ext = self.module.splitext(self.name) return base + @property + def namebase(self): + warnings.warn("Use .stem instead of .namebase", DeprecationWarning) + return self.stem + @property def ext(self): """ The file extension, for example ``'.py'``. """ From b5aa31291f79f80baf5ad6af2d119e88f050dfc8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Feb 2018 11:16:02 -0500 Subject: [PATCH 129/835] Add with_suffix method with tests. Ref #134. --- CHANGES.rst | 6 ++++++ path.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a9150c0d..276b424b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ - Renamed ``namebase`` to ``stem`` to match API of pathlib. Kept ``namebase`` as a deprecated alias for compatibility. +- Added new ``with_suffix`` method, useful for renaming the + extension on a Path:: + + orig = Path('mydir/mypath.bat') + renamed = orig.rename(orig.with_suffix('.cmd')) + 10.5 ---- diff --git a/path.py b/path.py index fc3d6de6..f6fcd9f5 100644 --- a/path.py +++ b/path.py @@ -336,6 +336,25 @@ def ext(self): f, ext = self.module.splitext(self) return ext + def with_suffix(self, suffix): + """ Return a new path with the file suffix changed (or added, if none) + + >>> Path('/home/guido/python.tar.gz').with_suffix(".foo") + Path('/home/guido/python.tar.foo') + + >>> Path('python').with_suffix('.zip') + Path('python.zip') + + >>> Path('filename.ext').with_suffix('zip') + Traceback (most recent call last): + ... + ValueError: Invalid suffix 'zip' + """ + if not suffix.startswith('.'): + raise ValueError("Invalid suffix {suffix!r}".format(**locals())) + + return self.stripext() + suffix + @property def drive(self): """ The drive specifier, for example ``'C:'``. From dd782d28b13bfb07a48563a434f48e94e7635eb8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Feb 2018 11:29:47 -0500 Subject: [PATCH 130/835] Drop support for Python 3.3 --- .travis.yml | 2 +- CHANGES.rst | 5 +++++ setup.py | 6 +----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ba55b21b..ef17d661 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: python python: - 2.7 -- 3.3 +- 3.4 - &latest_py3 3.6 jobs: diff --git a/CHANGES.rst b/CHANGES.rst index 276b424b..3bc06fff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +11.0 +---- + +- Drop support for Python 3.3. + 10.6 ---- diff --git a/setup.py b/setup.py index c4584edd..b26dce61 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ long_description=long_description, url="https://github.com/jaraco/" + name, py_modules=['path', 'test_path'], - python_requires='>=2.7', + python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ ], extras_require={ @@ -58,10 +58,6 @@ "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Topic :: Software Development :: Libraries :: Python Modules", ], entry_points={ From 290a4302cf554c6c983d92c7b2fc64e468bcca7a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Feb 2018 11:44:20 -0500 Subject: [PATCH 131/835] Correct typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 353c28a4..272b6cc2 100644 --- a/README.rst +++ b/README.rst @@ -56,7 +56,7 @@ One may have imagined ``pathlib`` would supersede ``path.py``. But the implementation and the usage quickly diverge, and ``path.py`` has several advantages over ``pathlib``: -- ``path.py`` implementsbits ``Path`` objects as a subclass of +- ``path.py`` implements ``Path`` objects as a subclass of ``str`` (unicode on Python 2), and as a result these ``Path`` objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one From c42f94223a2efd5d5614875cf4b17db855806a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Leli=C3=A8vre?= Date: Wed, 14 Feb 2018 02:09:28 -0600 Subject: [PATCH 132/835] Update README.rst: fix typos Fix minor typos in the README. --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 272b6cc2..214384c0 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ .. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest :target: https://pathpy.readthedocs.io/en/latest/?badge=latest -``path.py`` implements a path objects as first-class entities, allowing +``path.py`` implements path objects as first-class entities, allowing common operations on files to be invoked on those path objects directly. For example: @@ -22,12 +22,12 @@ example: ``path.py`` is `hosted at Github `_. -Find `the documentatation here `_. +Find `the documentation here `_. Guides and Testimonials ======================= -Yasoob has written the Python 101 `Writing a Cleanup Script +Yasoob wrote the Python 101 `Writing a Cleanup Script `_ based on ``path.py``. @@ -61,13 +61,13 @@ has several advantages over ``pathlib``: objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one must first cast values to strings before passing them to - APIs unaware of pathlib. + APIs unaware of ``pathlib``. - ``path.py`` goes beyond exposing basic functionality of a path and exposes commonly-used behaviors on a path, providing methods like ``rmtree`` (from shlib) and ``remove_p`` (remove a file if it exists). - As a PyPI-hosted package, ``path.py`` is free to iterate - more quickly than a stdlib package. Contributions are welcomed + faster than a stdlib package. Contributions are welcome and encouraged. Alternatives @@ -82,7 +82,7 @@ class, which shares some behaviors and interfaces with ``path.py``. Development =========== -To install an in-development version, use the Github links to clone or +To install a development version, use the Github links to clone or download a snapshot of the latest code. Alternatively, if you have git installed, you may be able to use ``pip`` to install directly from the repository:: From a8f66602f22459be95f8463e8cf6de1e653b352c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Feb 2018 08:45:54 -0500 Subject: [PATCH 133/835] Save the pip cache across builds. Ref pypa/setuptools#1279. --- appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 3d55a92b..2b7808f9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,6 +14,9 @@ install: build: off +cache: + - '%LOCALAPPDATA%\pip\Cache' + test_script: - "python -m pip install tox tox-venv" - "tox" From 41b814aa6cff3c46788a1d410095061a82af2076 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Feb 2018 09:43:16 -0500 Subject: [PATCH 134/835] Add workaround for build failures on Python 3.7 (yaml/pyyaml#126). --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index df1b0eff..47eae514 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 + # workaround for yaml/pyyaml#126 + # git+https://github.com/yaml/pyyaml@master#egg=pyyaml commands = py.test {posargs} python setup.py checkdocs From 40da2c65007ccc250c7d897d497ef2b3fb58f3d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Mar 2018 13:33:07 -0500 Subject: [PATCH 135/835] Run flake8 with tests. Add flake8 config to ignore common exclusions. Add comments to testing and docs extras to aid with merges. --- .flake8 | 2 ++ pytest.ini | 2 +- setup.py | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..e9955e71 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = W191,W503 diff --git a/pytest.ini b/pytest.ini index 1b2f6247..0ba22c33 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules +addopts=--doctest-modules --flake8 doctest_optionflags=ALLOW_UNICODE ELLIPSIS diff --git a/setup.py b/setup.py index 9e73e231..c5ad4b1b 100644 --- a/setup.py +++ b/setup.py @@ -36,14 +36,21 @@ ], extras_require={ 'testing': [ + # upstream 'pytest>=2.8', 'pytest-sugar>=0.9.1', 'collective.checkdocs', + 'pytest-flake8', + + # local ], 'docs': [ + # upstream 'sphinx', 'jaraco.packaging>=3.2', 'rst.linker>=1.9', + + # local ], }, setup_requires=[ From d5d22342cfd4bd0ebebefa98765ae1dfc5770bb2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 5 Mar 2018 09:50:36 -0500 Subject: [PATCH 136/835] Add appveyor badge (commented). Disable RTD by default. --- README.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 043561df..78750be7 100644 --- a/README.rst +++ b/README.rst @@ -6,5 +6,8 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: https://travis-ci.org/jaraco/skeleton -.. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest - :target: https://skeleton.readthedocs.io/en/latest/?badge=latest +.. .. image:: https://img.shields.io/appveyor/ci/jaraco/skeleton/master.svg +.. :target: https://ci.appveyor.com/project/jaraco/skeleton/branch/master + +.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest +.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest From cd31f81d564c1cd93c7cbf0b1b19c034e310ad52 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 7 Mar 2018 17:28:14 -0500 Subject: [PATCH 137/835] Limit workaround to affected Python --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 47eae514..c2bac458 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ minversion = 2.4 deps = setuptools>=31.0.1 # workaround for yaml/pyyaml#126 - # git+https://github.com/yaml/pyyaml@master#egg=pyyaml + # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" commands = py.test {posargs} python setup.py checkdocs From 31ad6d307c39e72bc98a8c6c140dc21703d4e15d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 26 Mar 2018 14:20:07 -0400 Subject: [PATCH 138/835] Apply 'approx' test to all Unix systems. Fixes #136. --- test_path.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_path.py b/test_path.py index 74208708..3270c168 100644 --- a/test_path.py +++ b/test_path.py @@ -329,13 +329,13 @@ def test_touch(self, tmpdir): # On Windows, "ctime" is CREATION time assert ct == ct2 assert ct2 < t2 - elif mac_version('10.13'): - # On macOS High Sierra, f.mtime will be close - assert ct2 == pytest.approx(f.mtime, 0.001) else: - # On other systems, it might be the CHANGE time - # (especially on Unix, time of inode changes) - assert ct == ct2 or ct2 == f.mtime + assert ( + # ctime is unchanged + ct == ct2 or + # ctime is approximately the mtime + ct2 == pytest.approx(f.mtime, 0.001) + ) def test_listing(self, tmpdir): d = Path(tmpdir) From b82eeaa35ca587a5446dc3fb16a9d54657390075 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 26 Mar 2018 14:33:23 -0400 Subject: [PATCH 139/835] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3bc06fff..e96bf449 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +11.0.1 +------ + +- #136: Fixed test failures on BSD. + +- Refreshed package metadata. + 11.0 ---- From 021b18b89cf83977397350ebe54603032086baf6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 27 Mar 2018 15:57:25 -0400 Subject: [PATCH 140/835] Bump minimum pytest version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5ad4b1b..62211249 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ extras_require={ 'testing': [ # upstream - 'pytest>=2.8', + 'pytest>=3.5', 'pytest-sugar>=0.9.1', 'collective.checkdocs', 'pytest-flake8', From e302df43fc90a3db2bc9119c9e0dad08a754c0f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 10 Apr 2018 09:19:48 -0400 Subject: [PATCH 141/835] Add pyproject.toml declaring build dependencies. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..3ef243cc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools_scm>=1.15"] From 9f06de212eb53c35ea52781796c58761fcd06de3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 12 Apr 2018 11:36:04 -0400 Subject: [PATCH 142/835] When ignoring linter warnings, document the reason. --- .flake8 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index e9955e71..df5f5271 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,6 @@ [flake8] -ignore = W191,W503 +ignore = + # Allow tabs for indentation + W191 + # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 + W503 From f7a4a46a61fed5610d3b3882252a885152e59d49 Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Wed, 9 May 2018 14:48:30 +0200 Subject: [PATCH 143/835] Improve doc examples. Both the `with` and '/` features are very useful, but hard to find if you don't know they exist :) --- README.rst | 12 ++++++++++++ path.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.rst b/README.rst index 15219e68..aa9cca21 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,18 @@ example: for f in d.files('*.py'): f.chmod(0o755) + # Globbing + for f in d.files('*.py'): + f.chmod(0o755) + + # Changing the working directory: + with Path("somewhere"): + # cwd in now `somewhere` + ... + + # Concatenate paths with / + foo_txt = Path("bar") / "foo.txt" + ``path.py`` is `hosted at Github `_. Find `the documentation here `_. diff --git a/path.py b/path.py index f6fcd9f5..ac8ec907 100644 --- a/path.py +++ b/path.py @@ -29,8 +29,18 @@ from path import Path d = Path('/home/guido/bin') + + # Globbing for f in d.files('*.py'): f.chmod(0o755) + + # Changing the working directory: + with Path("somewhere"): + # cwd in now `somewhere` + ... + + # Concatenate paths with / + foo_txt = Path("bar") / "foo.txt" """ from __future__ import unicode_literals From 53c017416d74f96c49fde361c0a5b774ceac00c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 9 Jun 2018 15:31:39 -0400 Subject: [PATCH 144/835] Disable the (broken) IPv6 in Travis. Ref travis-ci/travis-ci#8361. --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 85540ec0..e22ab6ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,4 +31,9 @@ cache: pip install: - pip install tox tox-venv +before_script: + # Disable IPv6. Ref travis-ci/travis-ci#8361 + - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; + fi script: tox From a4bdfe7caddd6b1bf4149f2d02adee727168ff8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jun 2018 08:59:18 -0400 Subject: [PATCH 145/835] Don't match issues if preceeded by some other indicator. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 14744ee8..aeda56c0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ ), replace=[ dict( - pattern=r'(Issue )?#(?P\d+)', + pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( From 7c9ad053c3ef0b66cc71431fb619da8e3a12bc26 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 14 Jun 2018 08:25:24 -0400 Subject: [PATCH 146/835] skip_upload_docs is default --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e22ab6ff..2560fc39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,6 @@ jobs: secure: ... # encrypt password with `travis encrypt` distributions: dists skip_cleanup: true - skip_upload_docs: true cache: pip From 67c79e3182614e96fd4cf3a4813932b1edeff262 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 14 Jun 2018 12:23:57 -0400 Subject: [PATCH 147/835] Drop the dot; http://blog.pytest.org/2016/whats-new-in-pytest-30/ --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c2bac458..c6c14f07 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ deps = # workaround for yaml/pyyaml#126 # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" commands = - py.test {posargs} + pytest {posargs} python setup.py checkdocs usedevelop = True extras = testing From 440adac3c3f91519a1ff47114774dbd1d5baf676 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 14 Jun 2018 15:09:33 -0400 Subject: [PATCH 148/835] Rely on declarative config to create long_description. --- pyproject.toml | 2 +- setup.cfg | 1 + setup.py | 6 ------ 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ef243cc..1af54cbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_scm>=1.15"] +requires = ["setuptools>=30.3", "wheel", "setuptools_scm>=1.15"] diff --git a/setup.cfg b/setup.cfg index 378a8e4f..ff90351b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,4 @@ universal = 1 [metadata] license_file = LICENSE +long_description = file:README.rst diff --git a/setup.py b/setup.py index 62211249..4afc6282 100644 --- a/setup.py +++ b/setup.py @@ -2,13 +2,8 @@ # Project skeleton maintained at https://github.com/jaraco/skeleton -import io - import setuptools -with io.open('README.rst', encoding='utf-8') as readme: - long_description = readme.read() - name = 'skeleton' description = '' nspkg_technique = 'native' @@ -23,7 +18,6 @@ author="Jason R. Coombs", author_email="jaraco@jaraco.com", description=description or name, - long_description=long_description, url="https://github.com/jaraco/" + name, packages=setuptools.find_packages(), include_package_data=True, From 15024f12b5d4e90aee4f9a780efa263f47865d96 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 27 Jun 2018 21:34:07 -0400 Subject: [PATCH 149/835] Remove workaround for pyyaml 126. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index c6c14f07..41e20a33 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,6 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 - # workaround for yaml/pyyaml#126 - # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" commands = pytest {posargs} python setup.py checkdocs From f8462db925cbfa2ca0721c84376c23026633a730 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 3 Jul 2018 11:13:00 -0400 Subject: [PATCH 150/835] Revert "Remove workaround for pyyaml 126." This reverts commit 15024f12b5d4e90aee4f9a780efa263f47865d96. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 41e20a33..c6c14f07 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 + # workaround for yaml/pyyaml#126 + # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" commands = pytest {posargs} python setup.py checkdocs From ed475c925d31ec146979f12b0ebf1e1021335e31 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 3 Jul 2018 11:15:33 -0400 Subject: [PATCH 151/835] We're getting close, but Python 3.7 still requires a workaround --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c6c14f07..5925d3e1 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ minversion = 2.4 deps = setuptools>=31.0.1 # workaround for yaml/pyyaml#126 - # git+https://github.com/yaml/pyyaml@master#egg=pyyaml;python_version=="3.7" + # pyyaml>=4.2b2;python_version=="3.7" commands = pytest {posargs} python setup.py checkdocs From 78f72eb2ec369b06f1fbc9c978488cf88760ef4e Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Tue, 10 Jul 2018 19:54:24 +0200 Subject: [PATCH 152/835] Fix sphinx warning when building changelog --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e96bf449..b10c8814 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -99,7 +99,8 @@ --- - Refreshed project metadata based on `jaraco's project - skeleton _. + skeleton `_. + - Releases are now automatically published via Travis-CI. - #111: More aggressively trap errors when importing ``pkg_resources``. From be595037f545388e71d269d524e03b0eb52c98f4 Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Tue, 10 Jul 2018 19:54:49 +0200 Subject: [PATCH 153/835] Fix warning about `_static` path not existing --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 14288531..ee387116 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ pygments_style = 'sphinx' html_theme = 'alabaster' -html_static_path = ['_static'] +html_static_path = [] htmlhelp_basename = 'pathpydoc' templates_path = ['_templates'] exclude_patterns = ['_build'] From b0a741400935c10614e87487c9e61d3ed4fcf09e Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Tue, 10 Jul 2018 19:55:26 +0200 Subject: [PATCH 154/835] Redirect users looking for recursive globs to walk* methods. --- path.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/path.py b/path.py index ac8ec907..7cbbd383 100644 --- a/path.py +++ b/path.py @@ -749,6 +749,10 @@ def glob(self, pattern): of all the files users have in their :file:`bin` directories. .. seealso:: :func:`glob.glob` + + .. note:: Glob is **not** recursive, even when using ``**``. + To do recursive globbing see :func:`walk`, + :func:`walkdirs` or :func:`walkfiles`. """ cls = self._next_class return [cls(s) for s in glob.glob(self / pattern)] From beb0e0eb774dd15e79574ace338b885101d86d4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 Aug 2018 15:14:55 -0400 Subject: [PATCH 155/835] Use xenial to include support for Python 3.7. --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2560fc39..b54e8e52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ -dist: trusty +dist: xenial sudo: false language: python python: - 2.7 -- &latest_py3 3.6 +- 3.6 +- &latest_py3 3.7 jobs: fast_finish: true From 5d245bb8ca22194dbf17e69f7db5f082101f931c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Aug 2018 17:15:48 -0400 Subject: [PATCH 156/835] Remove release, no longer needed. Use twine instead. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ff90351b..8fdad4fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,4 @@ [aliases] -release = dists upload dists = clean --all sdist bdist_wheel [bdist_wheel] From bf8c57034ab857eb5b642fbc137a811276e8067a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Aug 2018 17:17:12 -0400 Subject: [PATCH 157/835] Also ignore W504 in flake8, following the indication in OCA/maintainer-quality-tools that neither W503 nor W504 are worthwhile in general. --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index df5f5271..c85d34a7 100644 --- a/.flake8 +++ b/.flake8 @@ -4,3 +4,5 @@ ignore = W191 # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 + # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 + W504 From 8cd0459a9012ad5070c5b2364d9835653e6d58b7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Aug 2018 08:54:42 -0400 Subject: [PATCH 158/835] Release of pyyaml 3.13 seems to have fixed install issues on Python 3.7. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5925d3e1..41e20a33 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,6 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 - # workaround for yaml/pyyaml#126 - # pyyaml>=4.2b2;python_version=="3.7" commands = pytest {posargs} python setup.py checkdocs From 5633116de34f53c892d1f2c6d0f7de14c965cfa7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Aug 2018 12:59:25 -0400 Subject: [PATCH 159/835] Block pytest 3.7.3 due to pytest-dev/pytest#3888. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4afc6282..ba0fb89a 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ extras_require={ 'testing': [ # upstream - 'pytest>=3.5', + 'pytest>=3.5,!=3.7.3', 'pytest-sugar>=0.9.1', 'collective.checkdocs', 'pytest-flake8', From 6fa2673313f47e22095922dcb7d550ae5db9fdf2 Mon Sep 17 00:00:00 2001 From: Seth M Morton Date: Mon, 3 Sep 2018 17:10:59 -0700 Subject: [PATCH 160/835] Rename tempdir to TempDir. For conformity to PEP8. --- path.py | 8 ++++---- test_path.py | 32 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/path.py b/path.py index 7cbbd383..1f2e119e 100644 --- a/path.py +++ b/path.py @@ -1414,7 +1414,7 @@ def merge_tree(self, dst, symlinks=False, *args, **kwargs): is not capable of storing a copy of the entire source tree. """ update = kwargs.pop('update', False) - with tempdir() as _temp_dir: + with TempDir() as _temp_dir: # first copy the tree to a stage directory to support # the parameters and behavior of copytree. stage = _temp_dir / str(hash(self)) @@ -1629,7 +1629,7 @@ def _next_class(cls): ) -class tempdir(Path): +class TempDir(Path): """ A temporary directory via :func:`tempfile.mkdtemp`, and constructed with the same parameters that you can use @@ -1637,7 +1637,7 @@ class tempdir(Path): Example: - with tempdir() as d: + with TempDir() as d: # do stuff with the Path object "d" # here the directory is deleted automatically @@ -1652,7 +1652,7 @@ def _next_class(cls): def __new__(cls, *args, **kwargs): dirname = tempfile.mkdtemp(*args, **kwargs) - return super(tempdir, cls).__new__(cls, dirname) + return super(TempDir, cls).__new__(cls, dirname) def __init__(self, *args, **kwargs): pass diff --git a/test_path.py b/test_path.py index 3270c168..ae9966e2 100644 --- a/test_path.py +++ b/test_path.py @@ -33,7 +33,7 @@ import packaging.version import path -from path import tempdir +from path import TempDir from path import CaseInsensitivePattern as ci from path import SpecialResolver from path import Multi @@ -271,7 +271,7 @@ def test_touch(self, tmpdir): class TestScratchDir: """ - Tests that run in a temporary directory (does not test tempdir class) + Tests that run in a temporary directory (does not test TempDir class) """ def test_context_manager(self, tmpdir): """Can be used as context manager for chdir.""" @@ -686,7 +686,7 @@ def testLinesep(eol): test('UTF-16') def test_chunks(self, tmpdir): - p = (tempdir() / 'test.txt').touch() + p = (TempDir() / 'test.txt').touch() txt = "0123456789" size = 5 p.write_text(txt) @@ -700,13 +700,13 @@ def test_chunks(self, tmpdir): reason="samefile not present", ) def test_samefile(self, tmpdir): - f1 = (tempdir() / '1.txt').touch() + f1 = (TempDir() / '1.txt').touch() f1.write_text('foo') - f2 = (tempdir() / '2.txt').touch() + f2 = (TempDir() / '2.txt').touch() f1.write_text('foo') - f3 = (tempdir() / '3.txt').touch() + f3 = (TempDir() / '3.txt').touch() f1.write_text('bar') - f4 = (tempdir() / '4.txt') + f4 = (TempDir() / '4.txt') f1.copyfile(f4) assert os.path.samefile(f1, f2) == f1.samefile(f2) @@ -872,7 +872,7 @@ def test_constructor(self): """ One should be able to readily construct a temporary directory """ - d = tempdir() + d = TempDir() assert isinstance(d, path.Path) assert d.exists() assert d.isdir() @@ -881,24 +881,24 @@ def test_constructor(self): def test_next_class(self): """ - It should be possible to invoke operations on a tempdir and get + It should be possible to invoke operations on a TempDir and get Path classes. """ - d = tempdir() + d = TempDir() sub = d / 'subdir' assert isinstance(sub, path.Path) d.rmdir() def test_context_manager(self): """ - One should be able to use a tempdir object as a context, which will + One should be able to use a TempDir object as a context, which will clean up the contents after. """ - d = tempdir() + d = TempDir() res = d.__enter__() assert res is d (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', tempdir) + assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(None, None, None) assert not d.exists() @@ -906,10 +906,10 @@ def test_context_manager_exception(self): """ The context manager will not clean up if an exception occurs. """ - d = tempdir() + d = TempDir() d.__enter__() (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', tempdir) + assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(TypeError, TypeError('foo'), None) assert d.exists() @@ -919,7 +919,7 @@ def test_context_manager_using_with(self): provide a temporry directory that will be deleted after that. """ - with tempdir() as d: + with TempDir() as d: assert d.isdir() assert not d.isdir() From 85fa7ee01a34bb93a6b9534c35a8ae243a3fe72a Mon Sep 17 00:00:00 2001 From: Seth M Morton Date: Mon, 3 Sep 2018 17:12:00 -0700 Subject: [PATCH 161/835] Add tempdir name for backwards compatibility. tempdir is just an alias to TempDir. --- path.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/path.py b/path.py index 1f2e119e..742fd379 100644 --- a/path.py +++ b/path.py @@ -1665,6 +1665,10 @@ def __exit__(self, exc_type, exc_value, traceback): self.rmtree() +# For backwards compatibility. +tempdir = TempDir + + def _multi_permission_mask(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. From 71ffcd6f6961983117a11d38c5c8db611092e667 Mon Sep 17 00:00:00 2001 From: Seth M Morton Date: Mon, 3 Sep 2018 21:24:59 -0700 Subject: [PATCH 162/835] Add TempDir to docs (and __all__). This officially exports TempDir and makes it a documented member of path.py. This is intended to resolve issue #142. --- path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path.py b/path.py index 742fd379..2837780b 100644 --- a/path.py +++ b/path.py @@ -110,7 +110,7 @@ def io_error_compat(): ############################################################################## -__all__ = ['Path', 'CaseInsensitivePattern'] +__all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] LINESEPS = ['\r\n', '\r', '\n'] @@ -1635,7 +1635,7 @@ class TempDir(Path): constructed with the same parameters that you can use as a context manager. - Example: + Example:: with TempDir() as d: # do stuff with the Path object "d" From 94c48fb5e0ce4b52e6ef2d08ff2d9a667480edb9 Mon Sep 17 00:00:00 2001 From: Seth M Morton Date: Mon, 3 Sep 2018 21:38:29 -0700 Subject: [PATCH 163/835] Fix incorrect of TempDir instance in a context manager. In the previous implementation of TempDir, entering a second context manager will create a second temporary directory. I believe this does not meet most user's expectations, which is that they wish to change the CWD to the newly created temporary directory. ```python >>> from path import TempDir >>> with TempDir() as td: # Create the temporary directory. ... with td: # Enter the temporary directory... right? ... # Wrong! It created another temporary directory! ... ``` This commit changes the return value of the `__enter__` dunder method from `self` to `self._next_class(self)`, which has the effect of making the above code example work as expected. --- path.py | 6 +++++- test_path.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/path.py b/path.py index 2837780b..aa6481b4 100644 --- a/path.py +++ b/path.py @@ -1658,7 +1658,11 @@ def __init__(self, *args, **kwargs): pass def __enter__(self): - return self + # TempDir should return a Path version of itself and not itself + # so that a second context manager does not create a second + # temporary directory, but rather changes CWD to the location + # of the temporary directory. + return self._next_class(self) def __exit__(self, exc_type, exc_value, traceback): if not exc_value: diff --git a/test_path.py b/test_path.py index ae9966e2..1516db1e 100644 --- a/test_path.py +++ b/test_path.py @@ -896,7 +896,7 @@ def test_context_manager(self): """ d = TempDir() res = d.__enter__() - assert res is d + assert res == path.Path(d) (d / 'somefile.txt').touch() assert not isinstance(d / 'somefile.txt', TempDir) d.__exit__(None, None, None) From ca6227513e8e27903a886bffb3ed3c25bd74498e Mon Sep 17 00:00:00 2001 From: Seth M Morton Date: Mon, 3 Sep 2018 21:46:05 -0700 Subject: [PATCH 164/835] Add iglob method to Path. Like glob.glob, but an iterator. --- path.py | 18 ++++++++++++++++++ test_path.py | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/path.py b/path.py index 7cbbd383..6ff89b30 100644 --- a/path.py +++ b/path.py @@ -757,6 +757,24 @@ def glob(self, pattern): cls = self._next_class return [cls(s) for s in glob.glob(self / pattern)] + def iglob(self, pattern): + """ Return an iterator of Path objects that match the pattern. + + `pattern` - a path relative to this directory, with wildcards. + + For example, ``Path('/users').iglob('*/bin/*')`` returns an + iterator of all the files users have in their :file:`bin` + directories. + + .. seealso:: :func:`glob.iglob` + + .. note:: Glob is **not** recursive, even when using ``**``. + To do recursive globbing see :func:`walk`, + :func:`walkdirs` or :func:`walkfiles`. + """ + cls = self._next_class + return (cls(s) for s in glob.iglob(self / pattern)) + # # --- Reading or writing an entire file at once. diff --git a/test_path.py b/test_path.py index 3270c168..ac2d7696 100644 --- a/test_path.py +++ b/test_path.py @@ -22,6 +22,7 @@ import sys import shutil import time +import types import ntpath import posixpath import textwrap @@ -358,6 +359,11 @@ def test_listing(self, tmpdir): assert d.glob('*') == [af] assert d.glob('*.html') == [] assert d.glob('testfile') == [] + + # .iglob matches .glob but as an iterator. + assert list(d.iglob('*')) == d.glob('*') + assert isinstance(d.iglob('*'), types.GeneratorType) + finally: af.remove() From eea253bcf733f0214521ad3314a4a2903df85953 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Sep 2018 08:06:09 -0400 Subject: [PATCH 165/835] Update changelog --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b10c8814..7f83af50 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +11.1.0 +------ + +- #143, #144: Add iglob method. +- #142, #145: Rename ``tempdir`` to ``TempDir`` and declare + it as part of ``__all__``. Retain ``tempdir`` for compatibility + for now. +- #145: ``TempDir.__enter__`` no longer returns the ``TempDir`` + instance, but instead returns a ``Path`` instance, suitable for + entering to change the current working directory. + 11.0.1 ------ From eddb4071f4d208499c3605ac9ea0a84c291912f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Sep 2018 10:16:11 -0400 Subject: [PATCH 166/835] Rely on FileNotFoundError on Python 3 --- path.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/path.py b/path.py index 6eef195e..27fca56d 100644 --- a/path.py +++ b/path.py @@ -94,6 +94,7 @@ text_type = __builtin__.unicode getcwdu = os.getcwdu map = itertools.imap + FileNotFoundError = OSError @contextlib.contextmanager @@ -1321,9 +1322,8 @@ def remove_p(self): file does not exist. """ try: self.unlink() - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.ENOENT: + except FileNotFoundError as exc: + if PY2 and exc.errno != errno.ENOENT: raise return self From 8446716702c9fe742f2ab6f014ce14f319f70756 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:21:33 -0400 Subject: [PATCH 167/835] Add test capturing expectation that path.py should import in less than 100ms. Ref #146. --- test_path.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test_path.py b/test_path.py index c594e5f2..39d2dab9 100644 --- a/test_path.py +++ b/test_path.py @@ -29,6 +29,9 @@ import platform import importlib import operator +import datetime +import subprocess +import re import pytest import packaging.version @@ -233,6 +236,27 @@ def test_joinpath_returns_same_type(self): assert res2 == 'foo/bar' +class TestPerformance: + def test_import_time(self, monkeypatch): + """ + Import of path.py should take less than 100ms. + + Run tests in a subprocess to isolate from test suite overhead. + """ + cmd = [ + sys.executable, + '-m', 'timeit', + '-n', '1', + '-r', '1', + 'import path', + ] + res = subprocess.check_output(cmd, universal_newlines=True) + dur = re.search(r'(\d+) msec per loop', res).group(1) + limit = datetime.timedelta(milliseconds=100) + duration = datetime.timedelta(milliseconds=int(dur)) + assert duration < limit + + class TestSelfReturn: """ Some methods don't necessarily return any value (e.g. makedirs, From 9e1f7e4d1a04b2d909bb23db8e717f0126745eb2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:39:42 -0400 Subject: [PATCH 168/835] Rely on importlib_metadata. Fixes #146. --- path.py | 4 ++-- setup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/path.py b/path.py index 27fca56d..cbdfee70 100644 --- a/path.py +++ b/path.py @@ -123,8 +123,8 @@ def io_error_compat(): try: - import pkg_resources - __version__ = pkg_resources.require('path.py')[0].version + import importlib_metadata + __version__ = importlib_metadata.version('path.py') except Exception: __version__ = 'unknown' diff --git a/setup.py b/setup.py index 0e4fbecf..72d998df 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ py_modules=['path', 'test_path'], python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ + 'importlib_metadata', ], extras_require={ 'testing': [ From 626a4d38f93955563d5e9608f4ea35cb2b748cc6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:42:29 -0400 Subject: [PATCH 169/835] Update changelog. Ref #146. --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7f83af50..e8527f86 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +11.2.0 +------ + +- #146: Rely on `importlib_metadata + `_ instead of + setuptools/pkg_resources to load the version of the module. + Added tests ensuring a <100ms import time for the ``path`` + module. This change adds an explicit dependency on the + importlib_metadata package, but the project still supports + copying of the ``path.py`` module without any dependencies. + 11.1.0 ------ From 0d53bf7d3d2368c7548e0d511ebb29a320f0164e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:43:23 -0400 Subject: [PATCH 170/835] Remove executable function of test_path. Tester is always expected to invoke pytest directly. --- test_path.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test_path.py b/test_path.py index 39d2dab9..3ba08bcc 100644 --- a/test_path.py +++ b/test_path.py @@ -1221,7 +1221,3 @@ def test_iteration(self): assert not isinstance(first, Multi) assert next(items) == '/baz/bing' assert path == input - - -if __name__ == '__main__': - pytest.main() From 6ea5fd69e04d246a9c8bef9d955c8988c266b8f2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:51:55 -0400 Subject: [PATCH 171/835] Add test capturing guarantee that path.py doesn't require any dependencies --- test_path.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test_path.py b/test_path.py index 3ba08bcc..bab3e360 100644 --- a/test_path.py +++ b/test_path.py @@ -1221,3 +1221,16 @@ def test_iteration(self): assert not isinstance(first, Multi) assert next(items) == '/baz/bing' assert path == input + + +def test_no_dependencies(): + """ + Path.py guarantees that the path module can be + transplanted into an environment without any dependencies. + """ + cmd = [ + sys.executable, + '-S', + '-c', 'import path', + ] + subprocess.check_call(cmd) From c43d1f562090f2832d4a69292b7f7b50527ba512 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 08:58:31 -0400 Subject: [PATCH 172/835] Mark test as xfail on Python 2.7 --- test_path.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test_path.py b/test_path.py index bab3e360..eb102141 100644 --- a/test_path.py +++ b/test_path.py @@ -1223,6 +1223,7 @@ def test_iteration(self): assert path == input +@pytest.mark.xfail('path.PY2', reason="Python 2 has no __future__") def test_no_dependencies(): """ Path.py guarantees that the path module can be From bc61fe0141c1d8a73230b68aee76a1188ccbb551 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 12:54:00 -0400 Subject: [PATCH 173/835] Add Dockerfile for running Linux tests locally --- .dockerignore | 1 + Dockerfile | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..172bf578 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.tox diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7b118564 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +from ubuntu:bionic +RUN apt update +RUN apt install -y python python-pip git +RUN python -m pip install tox +RUN mkdir /app +ENV LANG=C.UTF-8 +WORKDIR /app +COPY . . +CMD tox From 1576ce4ed8cf770923e3a9054b98680316999332 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 13:04:59 -0400 Subject: [PATCH 174/835] Restore support for decoding os.listdir results on Linux on Python 2. Fixes #130. --- path.py | 36 ++++++++++++++++++++++++++++++++++++ setup.py | 1 + test_path.py | 4 ---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/path.py b/path.py index cbdfee70..71ef481f 100644 --- a/path.py +++ b/path.py @@ -62,6 +62,7 @@ import distutils.dir_util import importlib import itertools +import platform try: import win32security @@ -1967,3 +1968,38 @@ def fnmatch(self, pattern, normcase=None): pattern, normcase = self.__prepare(pattern, normcase) return self.__fnmatch(pattern, normcase) + + +def patch_for_linux_python2(): + """ + As reported in #130, when Linux users create filenames + not in the file system encoding, it creates problems on + Python 2. This function attempts to patch the os module + to make it behave more like that on Python 3. + """ + if not PY2 or platform.system() != 'Linux': + return + + try: + import backports.os + except ImportError: + return + + class OS: + """ + The proxy to the os module + """ + def __init__(self, wrapped): + self._orig = wrapped + + def __getattr__(self, name): + return getattr(self._orig, name) + + def listdir(self, *args, **kwargs): + items = self._orig.listdir(*args, **kwargs) + return list(map(backports.os.fsdecode, items)) + + globals().update(os=OS(os)) + + +patch_for_linux_python2() diff --git a/setup.py b/setup.py index 72d998df..f6ef1f00 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ 'importlib_metadata', + 'backports.os; python_version=="2.7" and sys_platform=="linux2"', ], extras_require={ 'testing': [ diff --git a/test_path.py b/test_path.py index eb102141..06dd0b22 100644 --- a/test_path.py +++ b/test_path.py @@ -409,10 +409,6 @@ def test_listing(self, tmpdir): except Exception: pass - @pytest.mark.xfail( - platform.system() == 'Linux' and path.PY2, - reason="Can't decode bytes in FS. See #121", - ) @pytest.mark.xfail( mac_version('10.13'), reason="macOS disallows invalid encodings", From 9cdf6ef1f4401b1ec4b032f1b61c0f4f7fd78b8d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Sep 2018 10:08:54 -0400 Subject: [PATCH 175/835] Move most package config to declarative config --- setup.cfg | 38 ++++++++++++++++++++++++++++++++++++ setup.py | 58 +------------------------------------------------------ 2 files changed, 39 insertions(+), 57 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8fdad4fa..adaed86d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,4 +6,42 @@ universal = 1 [metadata] license_file = LICENSE +name = skeleton +author = Jason R. Coombs +author_email = jaraco@jaraco.com +description = skeleton long_description = file:README.rst +url = https://github.com/jaraco/skeleton +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + +[options] +packages = find: +include_package_data = true +python_requires = >=2.7 +install_requires = +setup_requires = setuptools_scm >= 1.15.0 + +[options.extras_require] +testing = + # upstream + pytest >= 3.5, !=3.7.3 + pytest-sugar >= 0.9.1 + collective.checkdocs + pytest-flake8 + + # local + +docs = + # upstream + sphinx + jaraco.packaging >= 3.2 + rst.linker >= 1.9 + + # local + +[options.entry_points] diff --git a/setup.py b/setup.py index ba0fb89a..c990c529 100644 --- a/setup.py +++ b/setup.py @@ -4,61 +4,5 @@ import setuptools -name = 'skeleton' -description = '' -nspkg_technique = 'native' -""" -Does this package use "native" namespace packages or -pkg_resources "managed" namespace packages? -""" - -params = dict( - name=name, - use_scm_version=True, - author="Jason R. Coombs", - author_email="jaraco@jaraco.com", - description=description or name, - url="https://github.com/jaraco/" + name, - packages=setuptools.find_packages(), - include_package_data=True, - namespace_packages=( - name.split('.')[:-1] if nspkg_technique == 'managed' - else [] - ), - python_requires='>=2.7', - install_requires=[ - ], - extras_require={ - 'testing': [ - # upstream - 'pytest>=3.5,!=3.7.3', - 'pytest-sugar>=0.9.1', - 'collective.checkdocs', - 'pytest-flake8', - - # local - ], - 'docs': [ - # upstream - 'sphinx', - 'jaraco.packaging>=3.2', - 'rst.linker>=1.9', - - # local - ], - }, - setup_requires=[ - 'setuptools_scm>=1.15.0', - ], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - ], - entry_points={ - }, -) if __name__ == '__main__': - setuptools.setup(**params) + setuptools.setup(use_scm_version=True) From 533b9cf72bee40c1889cae629ae53104cfcd642f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Sep 2018 15:48:05 -0400 Subject: [PATCH 176/835] Always use splitlines for splitting lines. Fixes #151. --- CHANGES.rst | 7 +++++++ path.py | 8 +------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e8527f86..4189a670 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +11.3.0 +------ + +- #151: No longer use two techniques for splitting lines. + Instead, unconditionally rely on io.open for universal + newlines support and always use splitlines. + 11.2.0 ------ diff --git a/path.py b/path.py index cbdfee70..a996f895 100644 --- a/path.py +++ b/path.py @@ -924,15 +924,9 @@ def lines(self, encoding=None, errors='strict', retain=True): translated to ``'\n'``. If ``False``, newline characters are stripped off. Default is ``True``. - This uses ``'U'`` mode. - .. seealso:: :meth:`text` """ - if encoding is None and retain: - with self.open('U') as f: - return f.readlines() - else: - return self.text(encoding, errors).splitlines(retain) + return self.text(encoding, errors).splitlines(retain) def write_lines(self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False): From 7807aff6db760b5a30aeea939ec399664dc0a78d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Sep 2018 22:06:04 -0400 Subject: [PATCH 177/835] Add test for __version__. Ref #150. --- test_path.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_path.py b/test_path.py index eb102141..05342b06 100644 --- a/test_path.py +++ b/test_path.py @@ -1235,3 +1235,11 @@ def test_no_dependencies(): '-c', 'import path', ] subprocess.check_call(cmd) + + +def test_version(): + """ + Under normal circumstances, path should present a + __version__. + """ + assert re.match(r'\d+\.\d+.*', path.__version__) From 3ad076ceadb738eb0f14ff639618b7ac1b721949 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Sep 2018 22:06:38 -0400 Subject: [PATCH 178/835] Require importlib_metadata 0.5. Fixes #150. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 72d998df..854f63cf 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ py_modules=['path', 'test_path'], python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ - 'importlib_metadata', + 'importlib_metadata>=0.5', ], extras_require={ 'testing': [ From f8a537f300727a87c1a4a663a5df8b6e05e1020d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 19 Sep 2018 22:31:38 -0400 Subject: [PATCH 179/835] Ignore pycodestyle warning. Seems it's not going to be fixed anytime soon. --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 0ba22c33..d0ba39d3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,5 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 doctest_optionflags=ALLOW_UNICODE ELLIPSIS +filterwarnings= + ignore:Possible nested set::pycodestyle:113 From d7c7242e4a4a05886d5f64b6f9ba3906b0f4a0b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Sep 2018 09:35:38 -0400 Subject: [PATCH 180/835] Fix appveyor badge. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index aa9cca21..f469b432 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,8 @@ .. image:: https://img.shields.io/travis/jaraco/path.py/master.svg :target: https://travis-ci.org/jaraco/path.py -.. image:: https://img.shields.io/appveyor/ci/jaraco/path.py/master.svg - :target: https://ci.appveyor.com/project/jaraco/path.py/branch/master +.. image:: https://img.shields.io/appveyor/ci/jaraco/path-py/master.svg + :target: https://ci.appveyor.com/project/jaraco/path-py/branch/master .. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest :target: https://pathpy.readthedocs.io/en/latest/?badge=latest From 86362bac8d54c73554752e61bac0aa5db2fe5c50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Sep 2018 09:48:14 -0400 Subject: [PATCH 181/835] Update Advantages section based on newer understanding. --- README.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f469b432..424d3e77 100644 --- a/README.rst +++ b/README.rst @@ -76,7 +76,9 @@ has several advantages over ``pathlib``: objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one must first cast values to strings before passing them to - APIs unaware of ``pathlib``. + APIs unaware of ``pathlib``. This shortcoming was `addressed + by PEP 519 `_, + in Python 3.6. - ``path.py`` goes beyond exposing basic functionality of a path and exposes commonly-used behaviors on a path, providing methods like ``rmtree`` (from shlib) and ``remove_p`` (remove @@ -84,6 +86,16 @@ has several advantages over ``pathlib``: - As a PyPI-hosted package, ``path.py`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. +- ``path.py`` provides a uniform abstraction over its Path object, + freeing the implementer to subclass it readily. One cannot + subclass a ``pathlib.Path`` to add functionality, but must + subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even + if one only wishes to add a ``__dict__`` to the subclass + instances. ``path.py`` instead allows the ``Path.module`` + object to be overridden by subclasses, defaulting to the + ``os.path``. Even advanced uses of ``path.Path`` that + subclass the model do not need to be concerned with + OS-specific nuances. Alternatives ============ From 106c755f83d701003afca33e0ba22c20c3a31c97 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 Sep 2018 18:08:59 -0400 Subject: [PATCH 182/835] Also ignore flake8 error --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index d0ba39d3..61dab3d4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,3 +4,4 @@ addopts=--doctest-modules --flake8 doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 + ignore:Using or importing the ABCs::flake8:410 From 13d5ecbfe77817bc55dd907a85c22e8384e29b34 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Sep 2018 10:26:25 +0200 Subject: [PATCH 183/835] Update changelog --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4189a670..7df4e639 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +11.4.0 +------ + +- #130: Path.py now supports non-decodable filenames on + Linux and Python 2, leveraging the + `backports.os `_ + package (as an optional dependency). Currently, only + ``listdir`` is patched, but other ``os`` primitives may + be patched similarly in the ``patch_for_linux_python2`` + function. + 11.3.0 ------ From 0752ed307b8b67260b9c3abf58837388587678f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 10:21:36 -0400 Subject: [PATCH 184/835] Replace use of distutils with a bespoke implementation of merge_tree, modeled after the copytree example (https://docs.python.org/3/library/shutil.html#copytree-example). Fixes #141. --- path.py | 64 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/path.py b/path.py index 934f5c8b..72a55d28 100644 --- a/path.py +++ b/path.py @@ -59,7 +59,6 @@ import re import contextlib import io -import distutils.dir_util import importlib import itertools import platform @@ -96,6 +95,7 @@ getcwdu = os.getcwdu map = itertools.imap FileNotFoundError = OSError + itertools.filterfalse = itertools.ifilterfalse @contextlib.contextmanager @@ -1412,7 +1412,12 @@ def chdir(self): cd = chdir - def merge_tree(self, dst, symlinks=False, *args, **kwargs): + def merge_tree( + self, dst, symlinks=False, + # * + update=False, + copy_function=shutil.copy2, + ignore=lambda dir, contents: []): """ Copy entire contents of self to dst, overwriting existing contents in dst with those in self. @@ -1420,26 +1425,43 @@ def merge_tree(self, dst, symlinks=False, *args, **kwargs): If the additional keyword `update` is True, each `src` will only be copied if `dst` does not exist, or `src` is newer than `dst`. - - Note that the technique employed stages the files in a temporary - directory first, so this function is not suitable for merging - trees with large files, especially if the temporary directory - is not capable of storing a copy of the entire source tree. - """ - update = kwargs.pop('update', False) - with TempDir() as _temp_dir: - # first copy the tree to a stage directory to support - # the parameters and behavior of copytree. - stage = _temp_dir / str(hash(self)) - self.copytree(stage, symlinks, *args, **kwargs) - # now copy everything from the stage directory using - # the semantics of dir_util.copy_tree - distutils.dir_util.copy_tree( - stage, - dst, - preserve_symlinks=symlinks, - update=update, + """ + def copy_newer(src, dst, follow_symlinks=True): + is_newer_dst = ( + self.module.exists(dst) + and self.module.getmtime(dst) >= self.module.getmtime(src) ) + if is_newer_dst: + return dst + return copy_function(src, dst, follow_symlinks=follow_symlinks) + + dst = self._next_class(dst) + dst.makedirs_p() + + copy_func = copy_newer if update else copy_function + + _ignored = ignore(self, os.listdir(self)) + + def ignored(item): + return item.name in _ignored + + for source in itertools.filterfalse(ignored, self.listdir()): + dest = dst / source.name + if symlinks and source.islink(): + target = source.readlink() + target.symlink(dest) + elif source.isdir(): + source.mergetree( + dest, + symlinks=symlinks, + update=update, + copy_function=copy_func, + ignore=ignore, + ) + else: + copy_func(source, dest) + + self.copystat(dst) # # --- Special stuff from os From 48247314f0c278e055a855b1bd911e2660a7a95b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 10:39:07 -0400 Subject: [PATCH 185/835] Extract 'only_newer' as a wrapper around the copy function --- path.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/path.py b/path.py index 72a55d28..17686e8d 100644 --- a/path.py +++ b/path.py @@ -1426,19 +1426,10 @@ def merge_tree( `src` will only be copied if `dst` does not exist, or `src` is newer than `dst`. """ - def copy_newer(src, dst, follow_symlinks=True): - is_newer_dst = ( - self.module.exists(dst) - and self.module.getmtime(dst) >= self.module.getmtime(src) - ) - if is_newer_dst: - return dst - return copy_function(src, dst, follow_symlinks=follow_symlinks) - dst = self._next_class(dst) dst.makedirs_p() - copy_func = copy_newer if update else copy_function + copy_func = only_newer(copy_function) if update else copy_function _ignored = ignore(self, os.listdir(self)) @@ -1596,6 +1587,23 @@ def special(cls): return functools.partial(SpecialResolver, cls) +def only_newer(copy_func): + """ + Wrap a copy function (like shutil.copy2) to return + the dst if it's newer than the source. + """ + @functools.wraps(copy_func) + def wrapper(src, dst, *args, **kwargs): + is_newer_dst = ( + dst.exists() + and dst.getmtime() >= src.getmtime() + ) + if is_newer_dst: + return dst + return copy_func(src, dst, *args, **kwargs) + return wrapper + + class SpecialResolver(object): class ResolverScope: def __init__(self, paths, scope): From 27fa624459eba0540ed3f355df58d0eae1629bfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 10:53:03 -0400 Subject: [PATCH 186/835] Add test demonstrating use (and coverage) of only_newer --- test_path.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test_path.py b/test_path.py index b3181838..0b866d6d 100644 --- a/test_path.py +++ b/test_path.py @@ -851,6 +851,20 @@ def test_copytree_parameters(self): assert self.subdir_b.isdir() assert self.subdir_b.listdir() == [self.subdir_b / self.test_file.name] + def test_only_newer(self): + """ + merge_tree should accept a copy_function in which only + newer files are copied and older files do not overwrite + newer copies in the dest. + """ + target = self.subdir_b / 'testfile.txt' + target.write_text('this is newer') + self.subdir_a.merge_tree( + self.subdir_b, + copy_function=path.only_newer(shutil.copy2), + ) + assert target.text() == 'this is newer' + class TestChdir: def test_chdir_or_cd(self, tmpdir): From 2377166e63ec8e8e18c7f8b790a4d257c14f8d46 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 10:42:25 -0400 Subject: [PATCH 187/835] Deprecated the update parameter to merge_tree and instead provide guidance on how to use the copy_function for this purpose. --- path.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/path.py b/path.py index 17686e8d..360d3f7b 100644 --- a/path.py +++ b/path.py @@ -1422,14 +1422,21 @@ def merge_tree( Copy entire contents of self to dst, overwriting existing contents in dst with those in self. - If the additional keyword `update` is True, each - `src` will only be copied if `dst` does not exist, - or `src` is newer than `dst`. + To avoid overwriting newer files, supply a copy function + wrapped in ``only_newer``. For example:: + + src.merge_tree(dst, copy_function=only_newer(shutil.copy2)) """ dst = self._next_class(dst) dst.makedirs_p() - copy_func = only_newer(copy_function) if update else copy_function + if update: + warnings.warn( + "Update is deprecated; " + "use copy_function=only_newer(shutil.copy2)", + DeprecationWarning, + ) + copy_function = only_newer(copy_function) _ignored = ignore(self, os.listdir(self)) @@ -1446,11 +1453,11 @@ def ignored(item): dest, symlinks=symlinks, update=update, - copy_function=copy_func, + copy_function=copy_function, ignore=ignore, ) else: - copy_func(source, dest) + copy_function(source, dest) self.copystat(dst) From 5725b542692849886bc9e298dc91bbaa9b34ef15 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Sep 2018 10:58:47 -0400 Subject: [PATCH 188/835] Avoid calling listdir twice for each dir in merge_tree --- path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/path.py b/path.py index 360d3f7b..e4df53fd 100644 --- a/path.py +++ b/path.py @@ -1438,12 +1438,13 @@ def merge_tree( ) copy_function = only_newer(copy_function) - _ignored = ignore(self, os.listdir(self)) + sources = self.listdir() + _ignored = ignore(self, [item.name for item in sources]) def ignored(item): return item.name in _ignored - for source in itertools.filterfalse(ignored, self.listdir()): + for source in itertools.filterfalse(ignored, sources): dest = dst / source.name if symlinks and source.islink(): target = source.readlink() From 3841ad4b6bf1ea45271bad2d010891d95254b57b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Sep 2018 10:41:55 +0200 Subject: [PATCH 189/835] Update changelog and documentation. Set stacklevel when raising DeprecationWarning. --- CHANGES.rst | 6 ++++++ path.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7df4e639..ffc5869b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,12 @@ be patched similarly in the ``patch_for_linux_python2`` function. +- #141: For merge_tree, instead of relying on the deprecated + distutils module, implement merge_tree explicitly. The + ``update`` parameter is deprecated, instead superseded + by a ``copy_function`` parameter and an ``only_newer`` + wrapper for any copy function. + 11.3.0 ------ diff --git a/path.py b/path.py index e4df53fd..eed3b816 100644 --- a/path.py +++ b/path.py @@ -1422,6 +1422,10 @@ def merge_tree( Copy entire contents of self to dst, overwriting existing contents in dst with those in self. + Pass ``symlinks=True`` to copy symbolic links as links. + + Accepts a ``copy_function``, similar to copytree. + To avoid overwriting newer files, supply a copy function wrapped in ``only_newer``. For example:: @@ -1435,6 +1439,7 @@ def merge_tree( "Update is deprecated; " "use copy_function=only_newer(shutil.copy2)", DeprecationWarning, + stacklevel=2, ) copy_function = only_newer(copy_function) From a5975072c235f0d68bf96870f798a0a32655256d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Sep 2018 10:43:29 +0200 Subject: [PATCH 190/835] Fix self-reference. Clearly this functionality isn't covered by tests. --- path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path.py b/path.py index eed3b816..fbc26233 100644 --- a/path.py +++ b/path.py @@ -1455,7 +1455,7 @@ def ignored(item): target = source.readlink() target.symlink(dest) elif source.isdir(): - source.mergetree( + source.merge_tree( dest, symlinks=symlinks, update=update, From 5ca6f6da77b6ba5daca8d0ef23afd1ddfaf28bc6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Sep 2018 10:27:10 +0200 Subject: [PATCH 191/835] Skip intermittently failing test on Python 2. Fixes #153. --- CHANGES.rst | 6 ++++++ test_path.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ffc5869b..dc45f6c8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +11.4.1 +------ + +- #153: Skip intermittently failing performance test on + Python 2. + 11.4.0 ------ diff --git a/test_path.py b/test_path.py index 0b866d6d..a2dcd235 100644 --- a/test_path.py +++ b/test_path.py @@ -237,6 +237,9 @@ def test_joinpath_returns_same_type(self): class TestPerformance: + @pytest.mark.skipif( + path.PY2, + reason="Tests fail frequently on Python 2; see #153") def test_import_time(self, monkeypatch): """ Import of path.py should take less than 100ms. From ba1dfe32b915384c326478966d016899fa19fcf0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 11:35:29 +0200 Subject: [PATCH 192/835] Replace 'walkdirs' and 'walkfiles' with simple filters on 'walk'. --- path.py | 173 ++++---------------------------------------------------- 1 file changed, 12 insertions(+), 161 deletions(-) diff --git a/path.py b/path.py index fbc26233..b0184045 100644 --- a/path.py +++ b/path.py @@ -639,89 +639,23 @@ def ignore(msg): for item in child.walk(pattern, errors): yield item - def walkdirs(self, pattern=None, errors='strict'): + def walkdirs(self, *args, **kwargs): """ D.walkdirs() -> iterator over subdirs, recursively. - - With the optional `pattern` argument, this yields only - directories whose names match the given pattern. For - example, ``mydir.walkdirs('*test')`` yields only directories - with names ending in ``'test'``. - - The `errors=` keyword argument controls behavior when an - error occurs. The default is ``'strict'``, which causes an - exception. The other allowed values are ``'warn'`` (which - reports the error via :func:`warnings.warn()`), and ``'ignore'``. """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.fnmatch(pattern): - yield child - for subsubdir in child.walkdirs(pattern, errors): - yield subsubdir + return ( + item + for item in self.walk(*args, **kwargs) + if item.isdir() + ) - def walkfiles(self, pattern=None, errors='strict'): + def walkfiles(self, *args, **kwargs): """ D.walkfiles() -> iterator over files in D, recursively. - - The optional argument `pattern` limits the results to files - with names that match the pattern. For example, - ``mydir.walkfiles('*.tmp')`` yields only files with the ``.tmp`` - extension. """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except Exception: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.fnmatch(pattern): - yield child - elif isdir: - for f in child.walkfiles(pattern, errors): - yield f + return ( + item + for item in self.walk(*args, **kwargs) + if item.isfile() + ) def fnmatch(self, pattern, normcase=None): """ Return ``True`` if `self.name` matches the given `pattern`. @@ -1889,89 +1823,6 @@ def __walk(self, pattern, normcase, errors): for item in child.__walk(pattern, normcase, errors): yield item - def walkdirs(self, pattern=None, errors='strict'): - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None - - return self.__walkdirs(pattern, normcase, errors) - - def __walkdirs(self, pattern, normcase, errors): - """ Prepared version of walkdirs """ - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - for subsubdir in child.__walkdirs(pattern, normcase, errors): - yield subsubdir - - def walkfiles(self, pattern=None, errors='strict'): - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None - - return self.__walkfiles(pattern, normcase, errors) - - def __walkfiles(self, pattern, normcase, errors): - """ Prepared version of walkfiles """ - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except Exception: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue - else: - raise - - if isfile: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - elif isdir: - for f in child.__walkfiles(pattern, normcase, errors): - yield f - def __fnmatch(self, pattern, normcase): """ Return ``True`` if `self.name` matches the given `pattern`, prepared version. From 0162645f4bf06b0aa84e0865f0bd3d7430dc4848 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 15:01:12 +0200 Subject: [PATCH 193/835] dirs and files now defers to 'listdir' signature --- path.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/path.py b/path.py index b0184045..fad35e0f 100644 --- a/path.py +++ b/path.py @@ -558,31 +558,27 @@ def listdir(self, pattern=None): if self._next_class(child).fnmatch(pattern) ] - def dirs(self, pattern=None): + def dirs(self, *args, **kwargs): """ D.dirs() -> List of this directory's subdirectories. The elements of the list are Path objects. This does not walk recursively into subdirectories (but see :meth:`walkdirs`). - With the optional `pattern` argument, this only lists - directories whose names match the given pattern. For - example, ``d.dirs('build-*')``. + Accepts parameters to :meth:`listdir`. """ - return [p for p in self.listdir(pattern) if p.isdir()] + return [p for p in self.listdir(*args, **kwargs) if p.isdir()] - def files(self, pattern=None): + def files(self, *args, **kwargs): """ D.files() -> List of the files in this directory. The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). - With the optional `pattern` argument, this only lists files - whose names match the given pattern. For example, - ``d.files('*.pyc')``. + Accepts parameters to :meth:`listdir`. """ - return [p for p in self.listdir(pattern) if p.isfile()] + return [p for p in self.listdir(*args, **kwargs) if p.isfile()] def walk(self, pattern=None, errors='strict'): """ D.walk() -> iterator over files and subdirs, recursively. From dca10b488f3a0b209292d5bfa3885775697f5b67 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 14:56:47 +0200 Subject: [PATCH 194/835] Deprecate pattern parameter and instead expect a match parameter for listdir and walk* --- path.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/path.py b/path.py index fad35e0f..203cc08f 100644 --- a/path.py +++ b/path.py @@ -62,6 +62,7 @@ import importlib import itertools import platform +import ntpath try: import win32security @@ -94,6 +95,7 @@ text_type = __builtin__.unicode getcwdu = os.getcwdu map = itertools.imap + filter = itertools.ifilter FileNotFoundError = OSError itertools.filterfalse = itertools.ifilterfalse @@ -175,6 +177,64 @@ def __get__(self, instance, owner): ) +class matchers(object): + # TODO: make this class a module + + @staticmethod + def from_pattern(pattern): + if pattern is None: + return + warnings.warn( + "'pattern' is deprecated, supply 'match' instead", + DeprecationWarning, + stacklevel=3, + ) + match = matchers.Pattern(pattern) + try: + match.normcase = pattern.normcase + except AttributeError: + pass + return match + + @staticmethod + def load(param): + """ + If the supplied parameter is a string, assum it's a simple + pattern. + """ + return ( + matchers.Pattern(param) if isinstance(param, string_types) + else param + ) + + class Base(object): + pass + + class Null(Base): + def __call__(self, path): + return True + + class Pattern(Base): + def __init__(self, pattern): + self.pattern = pattern + + def get_pattern(self, normcase): + try: + return self._pattern + except AttributeError: + pass + self._pattern = normcase(self.pattern) + return self._pattern + + def __call__(self, path): + normcase = getattr(self, 'normcase', path.module.normcase) + pattern = self.get_pattern(normcase) + return fnmatch.fnmatchcase(normcase(path.name), pattern) + + class CaseInsensitive(Pattern): + normcase = staticmethod(ntpath.normcase) + + class Path(text_type): """ Represents a filesystem path. @@ -537,7 +597,7 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching - def listdir(self, pattern=None): + def listdir(self, pattern=None, match=matchers.Null()): """ D.listdir() -> List of items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing @@ -545,18 +605,15 @@ def listdir(self, pattern=None): The elements of the list are Path objects. - With the optional `pattern` argument, this only lists - items whose names match the given pattern. + With the optional `match` argument, a callable, + only return items whose names match the given pattern. .. seealso:: :meth:`files`, :meth:`dirs` """ - if pattern is None: - pattern = '*' - return [ - self / child - for child in os.listdir(self) - if self._next_class(child).fnmatch(pattern) - ] + match = matchers.from_pattern(pattern) or matchers.load(match) + return list(filter(match, ( + self / child for child in os.listdir(self) + ))) def dirs(self, *args, **kwargs): """ D.dirs() -> List of this directory's subdirectories. @@ -580,7 +637,7 @@ def files(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isfile()] - def walk(self, pattern=None, errors='strict'): + def walk(self, pattern=None, errors='strict', match=matchers.Null()): """ D.walk() -> iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of @@ -610,6 +667,8 @@ def ignore(msg): raise ValueError("invalid errors parameter") errors = vars(Handlers).get(errors, errors) + match = matchers.from_pattern(pattern) or matchers.load(match) + try: childList = self.listdir() except Exception: @@ -620,7 +679,7 @@ def ignore(msg): return for child in childList: - if pattern is None or child.fnmatch(pattern): + if match(child): yield child try: isdir = child.isdir() @@ -632,7 +691,7 @@ def ignore(msg): isdir = False if isdir: - for item in child.walk(pattern, errors): + for item in child.walk(errors=errors, match=match): yield item def walkdirs(self, *args, **kwargs): From e49dad2897504755164af4069500ef1f67d213ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 20:55:48 +0200 Subject: [PATCH 195/835] Deprecate CaseInsensitivePattern and FastPath. Path should now have comparable performance characteristics. --- path.py | 169 +++++++++++--------------------------------------------- 1 file changed, 32 insertions(+), 137 deletions(-) diff --git a/path.py b/path.py index 203cc08f..a1599aee 100644 --- a/path.py +++ b/path.py @@ -180,22 +180,6 @@ def __get__(self, instance, owner): class matchers(object): # TODO: make this class a module - @staticmethod - def from_pattern(pattern): - if pattern is None: - return - warnings.warn( - "'pattern' is deprecated, supply 'match' instead", - DeprecationWarning, - stacklevel=3, - ) - match = matchers.Pattern(pattern) - try: - match.normcase = pattern.normcase - except AttributeError: - pass - return match - @staticmethod def load(param): """ @@ -204,7 +188,8 @@ def load(param): """ return ( matchers.Pattern(param) if isinstance(param, string_types) - else param + else param if param is not None + else matchers.Null() ) class Base(object): @@ -232,6 +217,17 @@ def __call__(self, path): return fnmatch.fnmatchcase(normcase(path.name), pattern) class CaseInsensitive(Pattern): + """ + A Pattern with a ``'normcase'`` property, suitable for passing to + :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, + :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. + + For example, to get all files ending in .py, .Py, .pY, or .PY in the + current directory:: + + from path import Path, matchers + Path('.').files(matchers.CaseInsensitive('*.py')) + """ normcase = staticmethod(ntpath.normcase) @@ -597,7 +593,7 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching - def listdir(self, pattern=None, match=matchers.Null()): + def listdir(self, match=None): """ D.listdir() -> List of items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing @@ -610,7 +606,7 @@ def listdir(self, pattern=None, match=matchers.Null()): .. seealso:: :meth:`files`, :meth:`dirs` """ - match = matchers.from_pattern(pattern) or matchers.load(match) + match = matchers.load(match) return list(filter(match, ( self / child for child in os.listdir(self) ))) @@ -637,7 +633,7 @@ def files(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isfile()] - def walk(self, pattern=None, errors='strict', match=matchers.Null()): + def walk(self, match=None, errors='strict'): """ D.walk() -> iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of @@ -667,7 +663,7 @@ def ignore(msg): raise ValueError("invalid errors parameter") errors = vars(Handlers).get(errors, errors) - match = matchers.from_pattern(pattern) or matchers.load(match) + match = matchers.load(match) try: childList = self.listdir() @@ -1792,125 +1788,24 @@ def _permission_mask(mode): return functools.partial(op_map[op], mask) -class CaseInsensitivePattern(text_type): - """ - A string with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, - :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. - - For example, to get all files ending in .py, .Py, .pY, or .PY in the - current directory:: - - from path import Path, CaseInsensitivePattern as ci - Path('.').files(ci('*.py')) - """ - - @property - def normcase(self): - return __import__('ntpath').normcase +class CaseInsensitivePattern(matchers.CaseInsensitive): + def __init__(self, value): + warnings.warn( + "Use matchers.CaseInsensitive instead", + DeprecationWarning, + stacklevel=2, + ) + super(CaseInsensitivePattern, self).__init__(value) class FastPath(Path): - """ - Performance optimized version of Path for use - on embedded platforms and other systems with limited - CPU. See #115 and #116 for background. - """ - - def listdir(self, pattern=None): - children = os.listdir(self) - if pattern is None: - return [self / child for child in children] - - pattern, normcase = self.__prepare(pattern) - return [ - self / child - for child in children - if self._next_class(child).__fnmatch(pattern, normcase) - ] - - def walk(self, pattern=None, errors='strict'): - class Handlers: - def strict(msg): - raise - - def warn(msg): - warnings.warn(msg, TreeWalkWarning) - - def ignore(msg): - pass - - if not callable(errors) and errors not in vars(Handlers): - raise ValueError("invalid errors parameter") - errors = vars(Handlers).get(errors, errors) - - if pattern: - pattern, normcase = self.__prepare(pattern) - else: - normcase = None - - return self.__walk(pattern, normcase, errors) - - def __walk(self, pattern, normcase, errors): - """ Prepared version of walk """ - try: - childList = self.listdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to list directory '%(self)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - return - - for child in childList: - if pattern is None or child.__fnmatch(pattern, normcase): - yield child - try: - isdir = child.isdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to access '%(child)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - isdir = False - - if isdir: - for item in child.__walk(pattern, normcase, errors): - yield item - - def __fnmatch(self, pattern, normcase): - """ Return ``True`` if `self.name` matches the given `pattern`, - prepared version. - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. The pattern is expected to be normcase'd - already. - `normcase` - A function used to normalize the pattern and - filename before matching. - .. seealso:: :func:`Path.fnmatch` - """ - return fnmatch.fnmatchcase(normcase(self.name), pattern) - - def __prepare(self, pattern, normcase=None): - """ Prepares a fmatch_pattern for use with ``FastPath.__fnmatch`. - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. If the pattern contains a `normcase` - attribute, it is applied to the name and path prior to comparison. - `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, - which defaults to :meth:`os.path.normcase`. - .. seealso:: :func:`FastPath.__fnmatch` - """ - if not normcase: - normcase = getattr(pattern, 'normcase', self.module.normcase) - pattern = normcase(pattern) - return pattern, normcase - - def fnmatch(self, pattern, normcase=None): - if not pattern: - raise ValueError("No pattern provided") - - pattern, normcase = self.__prepare(pattern, normcase) - return self.__fnmatch(pattern, normcase) + def __init__(self, *args, **kwargs): + warnings.warn( + "Use Path, as FastPath no longer holds any advantage", + DeprecationWarning, + stacklevel=2, + ) + super(FastPath, self).__init__(*args, **kwargs) def patch_for_linux_python2(): From e6e2a5b69a6f6f10e62a9730c036907cb86c6db4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 20:59:07 +0200 Subject: [PATCH 196/835] Only test Path, as FastPath is identical except for DeprecationWarnings. --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index a2dcd235..3eae457c 100644 --- a/test_path.py +++ b/test_path.py @@ -50,7 +50,7 @@ def p(**choices): return choices[os.name] -@pytest.fixture(autouse=True, params=[path.Path, path.FastPath]) +@pytest.fixture(autouse=True, params=[path.Path]) def path_class(request, monkeypatch): """ Invoke tests on any number of Path classes. From 311dfb1f91e9e6f56a5cd6fb08a9640a9b956419 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 21:01:00 +0200 Subject: [PATCH 197/835] Use new recommended method for walking case-insensitive. --- test_path.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_path.py b/test_path.py index 3eae457c..2a7ddb8f 100644 --- a/test_path.py +++ b/test_path.py @@ -38,7 +38,7 @@ import path from path import TempDir -from path import CaseInsensitivePattern as ci +from path import matchers from path import SpecialResolver from path import Multi @@ -1036,10 +1036,10 @@ def test_listdir_case_insensitive(self, tmpdir): p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir(ci('S*')) == [p / 'sub'] - assert p.listdir(ci('f*')) == [p / 'File'] - assert p.files(ci('S*')) == [] - assert p.dirs(ci('f*')) == [] + assert p.listdir(matchers.CaseInsensitive('S*')) == [p / 'sub'] + assert p.listdir(matchers.CaseInsensitive('f*')) == [p / 'File'] + assert p.files(matchers.CaseInsensitive('S*')) == [] + assert p.dirs(matchers.CaseInsensitive('f*')) == [] def test_walk_case_insensitive(self, tmpdir): p = Path(tmpdir) @@ -1048,7 +1048,7 @@ def test_walk_case_insensitive(self, tmpdir): (p / 'sub1' / 'foo' / 'bar.Txt').touch() (p / 'sub2' / 'foo' / 'bar.TXT').touch() (p / 'sub2' / 'foo' / 'bar.txt.bz2').touch() - files = list(p.walkfiles(ci('*.txt'))) + files = list(p.walkfiles(matchers.CaseInsensitive('*.txt'))) assert len(files) == 2 assert p / 'sub2' / 'foo' / 'bar.TXT' in files assert p / 'sub1' / 'foo' / 'bar.Txt' in files From 8da8f91571f293a9b2a6989566a7ec5daf76a047 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 21:14:55 +0200 Subject: [PATCH 198/835] Update changelog --- CHANGES.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index dc45f6c8..66a8916d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,16 @@ +11.5.0 +------ + +- #156: Re-wrote the handling of pattern matches for + ``listdir``, ``walk``, and related methods, allowing + the pattern to be a more complex object. This approach + drastically simplifies the code and obviates the + ``CaseInsensitivePattern`` and ``FastPath`` classes. + Now the main ``Path`` class should be as performant + as ``FastPath`` and case-insensitive matches can be + readily constructed using the new + ``path.matchers.CaseInsensitive`` class. + 11.4.1 ------ From 1bab5646c6e7143f98c8a4b8cf28d2f0e420a5d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 21:34:43 +0200 Subject: [PATCH 199/835] Remove duplicate license, covered by ./LICENSE --- path.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/path.py b/path.py index a1599aee..69ac5c13 100644 --- a/path.py +++ b/path.py @@ -1,25 +1,3 @@ -# -# Copyright (c) 2010 Mikhail Gusarov -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# - """ path.py - An object representing a path to a file or directory. From 4e574d1be73099350242418c2eb76d909b053721 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 21:29:53 +0200 Subject: [PATCH 200/835] Move 'path' to its own package --- path.py => path/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename path.py => path/__init__.py (99%) diff --git a/path.py b/path/__init__.py similarity index 99% rename from path.py rename to path/__init__.py index 69ac5c13..c73f46c1 100644 --- a/path.py +++ b/path/__init__.py @@ -766,7 +766,7 @@ def chunks(self, size, *args, **kwargs): :example: >>> hash = hashlib.md5() - >>> for chunk in Path("path.py").chunks(8192, mode='rb'): + >>> for chunk in Path("CHANGES.rst").chunks(8192, mode='rb'): ... hash.update(chunk) This will read the file by chunks of 8192 bytes. diff --git a/setup.py b/setup.py index 4ff2e044..442171ff 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ maintainer_email="jaraco@jaraco.com", description=description or name, url="https://github.com/jaraco/" + name, - py_modules=['path', 'test_path'], + packages=setuptools.find_packages(), python_requires='>=2.7,!=3.1,!=3.2,!=3.3', install_requires=[ 'importlib_metadata>=0.5', From 08faa9bc94a53e6f1344a90c2a1edb1a57aa3c0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 21:46:13 +0200 Subject: [PATCH 201/835] Move matchers to their own module. --- path/__init__.py | 57 ++----------------------------------- path/matchers.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 path/matchers.py diff --git a/path/__init__.py b/path/__init__.py index c73f46c1..132f7cea 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -40,7 +40,6 @@ import importlib import itertools import platform -import ntpath try: import win32security @@ -57,6 +56,8 @@ except ImportError: pass +from . import matchers + ############################################################################## # Python 2/3 support PY3 = sys.version_info >= (3,) @@ -155,60 +156,6 @@ def __get__(self, instance, owner): ) -class matchers(object): - # TODO: make this class a module - - @staticmethod - def load(param): - """ - If the supplied parameter is a string, assum it's a simple - pattern. - """ - return ( - matchers.Pattern(param) if isinstance(param, string_types) - else param if param is not None - else matchers.Null() - ) - - class Base(object): - pass - - class Null(Base): - def __call__(self, path): - return True - - class Pattern(Base): - def __init__(self, pattern): - self.pattern = pattern - - def get_pattern(self, normcase): - try: - return self._pattern - except AttributeError: - pass - self._pattern = normcase(self.pattern) - return self._pattern - - def __call__(self, path): - normcase = getattr(self, 'normcase', path.module.normcase) - pattern = self.get_pattern(normcase) - return fnmatch.fnmatchcase(normcase(path.name), pattern) - - class CaseInsensitive(Pattern): - """ - A Pattern with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, - :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. - - For example, to get all files ending in .py, .Py, .pY, or .PY in the - current directory:: - - from path import Path, matchers - Path('.').files(matchers.CaseInsensitive('*.py')) - """ - normcase = staticmethod(ntpath.normcase) - - class Path(text_type): """ Represents a filesystem path. diff --git a/path/matchers.py b/path/matchers.py new file mode 100644 index 00000000..d889772c --- /dev/null +++ b/path/matchers.py @@ -0,0 +1,74 @@ +from __future__ import absolute_import, unicode_literals + +import sys +import ntpath +import fnmatch + + +############################################################################## +# Python 2/3 support +PY3 = sys.version_info >= (3,) +PY2 = not PY3 + +string_types = str, + + +if PY2: + import __builtin__ + string_types = __builtin__.basestring, + +############################################################################## + + +def load(param): + """ + If the supplied parameter is a string, assum it's a simple + pattern. + """ + return ( + Pattern(param) if isinstance(param, string_types) + else param if param is not None + else Null() + ) + + +class Base(object): + pass + + +class Null(Base): + def __call__(self, path): + return True + + +class Pattern(Base): + def __init__(self, pattern): + self.pattern = pattern + + def get_pattern(self, normcase): + try: + return self._pattern + except AttributeError: + pass + self._pattern = normcase(self.pattern) + return self._pattern + + def __call__(self, path): + normcase = getattr(self, 'normcase', path.module.normcase) + pattern = self.get_pattern(normcase) + return fnmatch.fnmatchcase(normcase(path.name), pattern) + + +class CaseInsensitive(Pattern): + """ + A Pattern with a ``'normcase'`` property, suitable for passing to + :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, + :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. + + For example, to get all files ending in .py, .Py, .pY, or .PY in the + current directory:: + + from path import Path, matchers + Path('.').files(matchers.CaseInsensitive('*.py')) + """ + normcase = staticmethod(ntpath.normcase) From 59cceeb35bd1005a16bd9985e623b10c82527e58 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Oct 2018 23:24:08 -0400 Subject: [PATCH 202/835] Require setuptools 34.4 to support python_requires in declarative config. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1af54cbd..65d74263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [build-system] -requires = ["setuptools>=30.3", "wheel", "setuptools_scm>=1.15"] +requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] From 166b43e1429fa1b9b467da82109151222719cc20 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 7 Nov 2018 17:47:20 -0500 Subject: [PATCH 203/835] Add workaround for Frozenball/pytest-sugar#159. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 41e20a33..78cc7f9f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 + pytest-sugar-bugfix159 commands = pytest {posargs} python setup.py checkdocs From d0f07a4e7ad465b0935bf85da94b12b9b8cc2e77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 9 Nov 2018 22:17:36 -0500 Subject: [PATCH 204/835] Add black config, pre-commit including black, check code with black. --- .flake8 | 5 +++-- .pre-commit-config.yaml | 5 +++++ README.rst | 4 ++++ docs/conf.py | 44 ++++++++++++++++++++--------------------- pyproject.toml | 3 +++ pytest.ini | 2 +- setup.cfg | 1 + setup.py | 2 +- 8 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 index c85d34a7..790c109f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,8 +1,9 @@ [flake8] +max-line-length = 88 ignore = - # Allow tabs for indentation - W191 # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 W504 + # Black creates whitespace before colon + E203 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..922d9424 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/ambv/black + rev: 18.9b0 + hooks: + - id: black diff --git a/README.rst b/README.rst index 78750be7..7050da33 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,10 @@ .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: https://travis-ci.org/jaraco/skeleton +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + :alt: Code style: Black + .. .. image:: https://img.shields.io/appveyor/ci/jaraco/skeleton/master.svg .. :target: https://ci.appveyor.com/project/jaraco/skeleton/branch/master diff --git a/docs/conf.py b/docs/conf.py index aeda56c0..d9ea1a63 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,31 +2,31 @@ # -*- coding: utf-8 -*- extensions = [ - 'sphinx.ext.autodoc', - 'jaraco.packaging.sphinx', - 'rst.linker', + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', + 'rst.linker', ] master_doc = 'index' link_files = { - '../CHANGES.rst': dict( - using=dict( - GH='https://github.com', - ), - replace=[ - dict( - pattern=r'(Issue #|\B#)(?P\d+)', - url='{package_url}/issues/{issue}', - ), - dict( - pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', - with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', - ), - dict( - pattern=r'PEP[- ](?P\d+)', - url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', - ), - ], - ), + '../CHANGES.rst': dict( + using=dict( + GH='https://github.com', + ), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', + ), + ], + ), } diff --git a/pyproject.toml b/pyproject.toml index 65d74263..a8b44c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,5 @@ [build-system] requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] + +[tool.black] +skip-string-normalization = true diff --git a/pytest.ini b/pytest.ini index 61dab3d4..15bb8b72 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 +addopts=--doctest-modules --flake8 --black doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 diff --git a/setup.cfg b/setup.cfg index adaed86d..78a0e465 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ testing = pytest-sugar >= 0.9.1 collective.checkdocs pytest-flake8 + pytest-black # local diff --git a/setup.py b/setup.py index c990c529..3435b2ca 100644 --- a/setup.py +++ b/setup.py @@ -5,4 +5,4 @@ import setuptools if __name__ == '__main__': - setuptools.setup(use_scm_version=True) + setuptools.setup(use_scm_version=True) From 8a08fefa8561407bee150a7e6c0c9d5117ac5e7b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Nov 2018 12:29:28 -0500 Subject: [PATCH 205/835] Remove workaround for pytest-sugar 159, now fixed. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 78cc7f9f..41e20a33 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ minversion = 2.4 [testenv] deps = setuptools>=31.0.1 - pytest-sugar-bugfix159 commands = pytest {posargs} python setup.py checkdocs From 6de738440c6333e0f5e7b2447d2b5c05785481db Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Nov 2018 12:31:25 -0500 Subject: [PATCH 206/835] Remove pytest-sugar plugin from standard pipelines as recommended in Frozenball/pytest-sugar#159. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index adaed86d..2ea2224f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,6 @@ setup_requires = setuptools_scm >= 1.15.0 testing = # upstream pytest >= 3.5, !=3.7.3 - pytest-sugar >= 0.9.1 collective.checkdocs pytest-flake8 From 95af04d3fcf70a487f59c854d802d9bac193de53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Nov 2018 12:44:40 -0500 Subject: [PATCH 207/835] Prefer pytest-checkdocs to collective.checkdocs --- setup.cfg | 2 +- tox.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2ea2224f..30f3c087 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ setup_requires = setuptools_scm >= 1.15.0 testing = # upstream pytest >= 3.5, !=3.7.3 - collective.checkdocs + pytest-checkdocs pytest-flake8 # local diff --git a/tox.ini b/tox.ini index 41e20a33..4121a91f 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ deps = setuptools>=31.0.1 commands = pytest {posargs} - python setup.py checkdocs usedevelop = True extras = testing From 5c200dd4b98b91911ef9b7403373b84d017e42c0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Dec 2018 10:46:24 -0500 Subject: [PATCH 208/835] Suppress deprecation warning in docutils --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 61dab3d4..bbea8b12 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,3 +5,5 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 ignore:Using or importing the ABCs::flake8:410 + # workaround for https://sourceforge.net/p/docutils/bugs/348/ + ignore:'U' mode is deprecated::docutils.io From 2c91e8ec0d99f9ca354b7f913d61720925bb98bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Dec 2018 11:58:52 -0500 Subject: [PATCH 209/835] Remove use of setup_requires. Builders now require pip 10 or later to build/install from sdist. Older installers will still install the packages from wheels. Ref tox-dev/tox#809. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 30f3c087..e0395d78 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,6 @@ packages = find: include_package_data = true python_requires = >=2.7 install_requires = -setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] testing = From 216c4336ddb5b498e429219ef765fa1ae857febd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 11 Dec 2018 14:21:46 -0500 Subject: [PATCH 210/835] Revert "Remove use of setup_requires. Builders now require pip 10 or later to build/install from sdist. Older installers will still install the packages from wheels. Ref tox-dev/tox#809." This reverts commit 2c91e8ec0d99f9ca354b7f913d61720925bb98bc. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index e0395d78..30f3c087 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ packages = find: include_package_data = true python_requires = >=2.7 install_requires = +setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] testing = From 32b254dee01b5ef2b695ae04889af482c6cb28c3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Dec 2018 13:26:23 -0500 Subject: [PATCH 211/835] Indicate build backend of setuptools --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 65d74263..efae667c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,3 @@ [build-system] requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] +build-backend = 'setuptools.build_meta' From a8bca166266fa2eeab931f6f20eef8e50048dddf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Dec 2018 17:08:36 -0500 Subject: [PATCH 212/835] Add support for cutting releases without DPL and using pep517. --- .travis.yml | 19 +++++++------------ install-pip-master.py | 21 +++++++++++++++++++++ setup.cfg | 3 --- tox.ini | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 install-pip-master.py diff --git a/.travis.yml b/.travis.yml index b54e8e52..16363054 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,18 +13,13 @@ jobs: - stage: deploy if: tag IS present python: *latest_py3 - install: skip - script: skip - deploy: - provider: pypi - on: - tags: true - all_branches: true - user: jaraco - password: - secure: ... # encrypt password with `travis encrypt` - distributions: dists - skip_cleanup: true + before_script: skip + env: + - TWINE_USERNAME=jaraco + # TWINE_PASSWORD + - secure: ... # encrypt `TWINE_PASSWORD="{password}"` with `travis encrypt` + - TOX_TESTENV_PASSENV="TWINE_USERNAME TWINE_PASSWORD" + script: tox -e release cache: pip diff --git a/install-pip-master.py b/install-pip-master.py new file mode 100644 index 00000000..d62d20f3 --- /dev/null +++ b/install-pip-master.py @@ -0,0 +1,21 @@ +""" +In order to support installation of pep517 from source, +pip from master must be installed. +""" + +import subprocess +import sys + + +def main(): + cmd = [ + sys.executable, + '-m', 'pip', 'install', + 'git+https://github.com/pypa/pip', + ] + subprocess.run(cmd) + cmd[-1:] = sys.argv[1:] + subprocess.run(cmd) + + +__name__ == '__main__' and main() diff --git a/setup.cfg b/setup.cfg index 30f3c087..726b307e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[aliases] -dists = clean --all sdist bdist_wheel - [bdist_wheel] universal = 1 diff --git a/tox.ini b/tox.ini index 4121a91f..853d7def 100644 --- a/tox.ini +++ b/tox.ini @@ -17,3 +17,18 @@ extras = changedir = docs commands = python -m sphinx . {toxinidir}/build/html + +[testenv:release] +skip_install = True +# workaround for pep517 build support +install_command = python install-pip-master.py {opts} {packages} +deps = + # pull from feature branch for feature + git+https://github.com/pypa/pep517@feature/build-command + # workaround for https://github.com/pypa/twine/issues/423 + git+https://github.com/pypa/twine + path.py +commands = + python -c "import path; path.Path('dist').rmtree_p()" + python -m pep517.build . + python -m twine upload dist/* From bc8a6cdf948376e1c846a121a4e8e4a699c66909 Mon Sep 17 00:00:00 2001 From: Sebastian Kriems Date: Fri, 14 Dec 2018 16:19:36 +0100 Subject: [PATCH 213/835] spaces, style and formatters (#4) use spaces, fixed indentation, format using autopep8 --- docs/conf.py | 44 +++++++++++++++++++------------------------- setup.py | 4 ++-- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index aeda56c0..49a855ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,32 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -extensions = [ - 'sphinx.ext.autodoc', - 'jaraco.packaging.sphinx', - 'rst.linker', -] +extensions = ["sphinx.ext.autodoc", "jaraco.packaging.sphinx", "rst.linker"] -master_doc = 'index' +master_doc = "index" link_files = { - '../CHANGES.rst': dict( - using=dict( - GH='https://github.com', - ), - replace=[ - dict( - pattern=r'(Issue #|\B#)(?P\d+)', - url='{package_url}/issues/{issue}', - ), - dict( - pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', - with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', - ), - dict( - pattern=r'PEP[- ](?P\d+)', - url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', - ), - ], - ), + "../CHANGES.rst": dict( + using=dict(GH="https://github.com"), + replace=[ + dict( + pattern=r"(Issue #|\B#)(?P\d+)", + url="{package_url}/issues/{issue}", + ), + dict( + pattern=r"^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n", + with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", + ), + dict( + pattern=r"PEP[- ](?P\d+)", + url="https://www.python.org/dev/peps/pep-{pep_number:0>4}/", + ), + ], + ) } diff --git a/setup.py b/setup.py index c990c529..50e9f0c4 100644 --- a/setup.py +++ b/setup.py @@ -4,5 +4,5 @@ import setuptools -if __name__ == '__main__': - setuptools.setup(use_scm_version=True) +if __name__ == "__main__": + setuptools.setup(use_scm_version=True) From 939c515f2cc01525cbbd71f26e71d21471abdc93 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Dec 2018 12:17:02 -0500 Subject: [PATCH 214/835] Rely on pep517 0.5 --- install-pip-master.py | 21 --------------------- tox.ini | 5 +---- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 install-pip-master.py diff --git a/install-pip-master.py b/install-pip-master.py deleted file mode 100644 index d62d20f3..00000000 --- a/install-pip-master.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -In order to support installation of pep517 from source, -pip from master must be installed. -""" - -import subprocess -import sys - - -def main(): - cmd = [ - sys.executable, - '-m', 'pip', 'install', - 'git+https://github.com/pypa/pip', - ] - subprocess.run(cmd) - cmd[-1:] = sys.argv[1:] - subprocess.run(cmd) - - -__name__ == '__main__' and main() diff --git a/tox.ini b/tox.ini index 853d7def..70b0be7a 100644 --- a/tox.ini +++ b/tox.ini @@ -20,11 +20,8 @@ commands = [testenv:release] skip_install = True -# workaround for pep517 build support -install_command = python install-pip-master.py {opts} {packages} deps = - # pull from feature branch for feature - git+https://github.com/pypa/pep517@feature/build-command + pep517>=0.5 # workaround for https://github.com/pypa/twine/issues/423 git+https://github.com/pypa/twine path.py From 192dafa3e9943e971a004d404be1b8e0d20691f2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Dec 2018 18:11:27 -0500 Subject: [PATCH 215/835] Add documentation on the skeleton. Fixes #5. --- .travis.yml | 2 +- setup.py | 2 - skeleton.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 skeleton.md diff --git a/.travis.yml b/.travis.yml index 16363054..8fc89320 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ jobs: env: - TWINE_USERNAME=jaraco # TWINE_PASSWORD - - secure: ... # encrypt `TWINE_PASSWORD="{password}"` with `travis encrypt` + - secure: ... - TOX_TESTENV_PASSENV="TWINE_USERNAME TWINE_PASSWORD" script: tox -e release diff --git a/setup.py b/setup.py index 50e9f0c4..827e955f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# Project skeleton maintained at https://github.com/jaraco/skeleton - import setuptools if __name__ == "__main__": diff --git a/skeleton.md b/skeleton.md new file mode 100644 index 00000000..bc78f37c --- /dev/null +++ b/skeleton.md @@ -0,0 +1,126 @@ +# Overview + +This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. + +## An SCM Managed Approach + +While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices. + +It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. + +The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. + +# Usage + +## new projects + +To use skeleton for a new project, simply pull the skeleton into a new project: + +``` +$ git init my-new-project +$ cd my-new-project +$ git pull gh://jaraco/skeleton +``` + +Now customize the project to suit your individual project needs. + +## existing projects + +If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. + +``` +$ git merge skeleton --allow-unrelated-histories +``` + +The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. + +## Updating + +Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations. + +Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. + +# Features + +The features/techniques employed by the skeleton include: + +- PEP 517/518 based build relying on setuptools as the build tool +- setuptools declarative configuration using setup.cfg +- tox for running tests +- A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out +- A CHANGES.rst file intended for publishing release notes about the project. + +## Packaging Conventions + +A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config). + +The setup.cfg file implements the following features: + +- Assumes universal wheel for release +- Advertises the project's LICENSE file (MIT by default) +- Reads the README.rst file into the long description +- Some common Trove classifiers +- Includes all packages discovered in the repo +- Data files in the package are also included (not just Python files) +- Declares the required Python versions +- Declares install requirements (empty by default) +- Declares setup requirements for legacy environments +- Supplies two 'extras': + - testing: requirements for running tests + - docs: requirements for building docs + - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project) +- Placeholder for defining entry points + +Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: + +- derive the project version from SCM tags +- ensure that all files committed to the repo are automatically included in releases + +## Running Tests + +The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). + +Other environments (invoked with `tox -e {name}`) supplied include: + + - a `build-docs` environment to build the documentation + - a `release` environment to publish the package to PyPI + +A pytest.ini is included to define common options around running tests. In particular: + +- rely on default test discovery in the current directory +- avoid recursing into common directories not containing tests +- run doctests on modules and invoke flake8 tests +- in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. +- filters out known warnings caused by libraries/functionality included by the skeleton + +Relies a .flake8 file to correct some default behaviors: + +- allow tabs for indentation (legacy for jaraco projects) +- disable mutually incompatible rules W503 and W504. + +## Continuous Integration + +The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. In addition to running tests, an additional deploy stage is configured to automatically release tagged commits. The username and password for PyPI must be configured for each project using the `travis` command and only after the travis project is created. As releases are cut with [twine](https://pypi.org/project/twine), the two values are supplied through the `TWINE_USERNAME` and `TWINE_PASSWORD`. To configure the latter as a secret, run the following command: + +``` +echo "TWINE_PASSWORD={password}" | travis encrypt +``` + +Or disable it in the CI definition and configure it through the web UI. + +Features include: +- test against Python 2 and 3 +- run on Ubuntu Xenial +- correct for broken IPv6 + +Also provided is a minimal template for running under Appveyor (Windows). + +## Building Documentation + +Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. + +In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. + +## Cutting releases + +By default, tagged commits are released through the continuous integration deploy stage. From 5b4c2503ce84744c0cdf398316d6b18863905297 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 27 Dec 2018 11:42:55 -0500 Subject: [PATCH 216/835] Add workaround for DeprecationWarning in flake8 --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index bbea8b12..9b3c1ecd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,3 +7,5 @@ filterwarnings= ignore:Using or importing the ABCs::flake8:410 # workaround for https://sourceforge.net/p/docutils/bugs/348/ ignore:'U' mode is deprecated::docutils.io + # workaround for https://gitlab.com/pycqa/flake8/issues/275 + ignore:You passed a bytestring as `filenames`.::flake8 From 8ac0f8736c746a829e6393ca5ba00fa8d042d426 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Dec 2018 21:57:02 -0500 Subject: [PATCH 217/835] Use consistent encoding quoting in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index efae667c..6f0a5168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] -build-backend = 'setuptools.build_meta' +build-backend = "setuptools.build_meta" From 4310c976400dc2eab8d8597b0dffaa7b787cff71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Dec 2018 22:00:08 -0500 Subject: [PATCH 218/835] Clarify purpose of local/upstream extras --- skeleton.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skeleton.md b/skeleton.md index bc78f37c..0e0c0eff 100644 --- a/skeleton.md +++ b/skeleton.md @@ -68,7 +68,7 @@ The setup.cfg file implements the following features: - Supplies two 'extras': - testing: requirements for running tests - docs: requirements for building docs - - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project) + - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts - Placeholder for defining entry points Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: From f3ff0541b6967ee91be3f572c5afe4f559436aa1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 12:12:55 -0500 Subject: [PATCH 219/835] Add Tidelift template --- README.rst | 11 +++++++++++ docs/_templates/tidelift-sidebar.html | 6 ++++++ docs/conf.py | 4 ++++ 3 files changed, 21 insertions(+) create mode 100644 README.rst create mode 100644 docs/_templates/tidelift-sidebar.html create mode 100644 docs/conf.py diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..5d77d8d3 --- /dev/null +++ b/README.rst @@ -0,0 +1,11 @@ +.. image:: https://tidelift.com/badges/github/GROUP/PROJECT + :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme + + +Security Contact +================ + +If you wish to report a security vulnerability, the public disclosure +of which may exacerbate the risk, please +`Contact Tidelift security `_, +which will coordinate the fix and disclosure privately. diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html new file mode 100644 index 00000000..c89c0f09 --- /dev/null +++ b/docs/_templates/tidelift-sidebar.html @@ -0,0 +1,6 @@ +

Professional support

+ +

+Professionally-supported {{ project }} is available with the +Tidelift Subscription. +

diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..d0287332 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,4 @@ + +# Custom sidebar templates, maps document names to template names. +templates_path = ['_templates'] +html_sidebars = {'index': 'tidelift-sidebar.html'} From c6655951aa8292127f01d53c337da1da642efe74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 13:18:15 -0500 Subject: [PATCH 220/835] Rely on alabaster theme to support sidebar rendering. --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index d0287332..3d109305 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,5 @@ # Custom sidebar templates, maps document names to template names. +html_theme = 'alabaster' templates_path = ['_templates'] html_sidebars = {'index': 'tidelift-sidebar.html'} From 12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Feb 2019 09:54:00 -0500 Subject: [PATCH 221/835] Suppress E117 as workaround for PyCQA/pycodestyle#836 --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index c85d34a7..04d2d97a 100644 --- a/.flake8 +++ b/.flake8 @@ -2,6 +2,8 @@ ignore = # Allow tabs for indentation W191 + # Workaround for https://github.com/PyCQA/pycodestyle/issues/836 + E117 # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 From 8186f76e906f80d678e895f6627afefee5617888 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Feb 2019 09:58:28 -0500 Subject: [PATCH 222/835] Amend skeleton documentation to expand on the value of the approach. --- skeleton.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skeleton.md b/skeleton.md index 0e0c0eff..09485cca 100644 --- a/skeleton.md +++ b/skeleton.md @@ -10,6 +10,8 @@ It's intended to be used by a new or existing project to adopt these practices a The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. +Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. + # Usage ## new projects From cdff6c8f0fa2a0439adc219a40bc2b11bb95d29d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Mar 2019 11:49:13 -0400 Subject: [PATCH 223/835] Remove sudo declaration in Travis config. See https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration for more details. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8fc89320..17d02624 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ dist: xenial -sudo: false language: python python: From 9edd5b701549833ebbfb354c072962c58e5394ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Mar 2019 19:54:23 -0400 Subject: [PATCH 224/835] Use nicer, simpler phrasing --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5d77d8d3..35762622 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,9 @@ .. image:: https://tidelift.com/badges/github/GROUP/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme - Security Contact ================ -If you wish to report a security vulnerability, the public disclosure -of which may exacerbate the risk, please -`Contact Tidelift security `_, -which will coordinate the fix and disclosure privately. +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. From 9d918244fb413126120d3824e7fa2b4546ca0c29 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2019 22:01:05 -0400 Subject: [PATCH 225/835] Update change log for 11.5.1 release --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b447c7f0..3da92fd9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +11.5.1 +------ + +- Minor packaging refresh. + 11.5.0 ------ From 7e9dbcc3943ba541b0ba8129af803a9ca25dadbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Apr 2019 01:21:31 -0400 Subject: [PATCH 226/835] Correct declaration to 'py_modules'. Fixes #163. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4d94eb51..a56b4943 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules [options] -pymodules = +py_modules = path python_requires = >=2.7,!=3.1,!=3.2,!=3.3 install_requires = From e374ca50814498f0b95b480ae38ac5cd284d5450 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Apr 2019 01:31:00 -0400 Subject: [PATCH 227/835] Update changelog for 11.5.2 release. Ref #163. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3da92fd9..421bb1b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +11.5.2 +------ + +- #163: Corrected 'pymodules' typo in package declaration. + 11.5.1 ------ From 0fdb0d4a0f7a4ec1ae582b72d0a3367568c29243 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Apr 2019 02:07:10 -0400 Subject: [PATCH 228/835] Drop support for Python 2. Fixes #148 --- .travis.yml | 1 - CHANGES.rst | 5 +++ appveyor.yml | 1 - path.py | 110 ++++++--------------------------------------------- setup.cfg | 4 +- test_path.py | 6 +-- 6 files changed, 20 insertions(+), 107 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5371dbed..57e76888 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ dist: xenial language: python python: -- 2.7 - 3.6 - &latest_py3 3.7 diff --git a/CHANGES.rst b/CHANGES.rst index 421bb1b2..9496806f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +12.0 +--- + +- #148: Dropped support for Python 2.7 and 3.4. + 11.5.2 ------ diff --git a/appveyor.yml b/appveyor.yml index 2b7808f9..9e367d85 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,6 @@ environment: matrix: - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python27-x64" install: # symlink python from a directory with a space diff --git a/path.py b/path.py index 69ac5c13..6bdb8b62 100644 --- a/path.py +++ b/path.py @@ -21,8 +21,6 @@ foo_txt = Path("bar") / "foo.txt" """ -from __future__ import unicode_literals - import sys import warnings import os @@ -39,7 +37,6 @@ import io import importlib import itertools -import platform import ntpath try: @@ -57,40 +54,6 @@ except ImportError: pass -############################################################################## -# Python 2/3 support -PY3 = sys.version_info >= (3,) -PY2 = not PY3 - -string_types = str, -text_type = str -getcwdu = os.getcwd - - -if PY2: - import __builtin__ - string_types = __builtin__.basestring, - text_type = __builtin__.unicode - getcwdu = os.getcwdu - map = itertools.imap - filter = itertools.ifilter - FileNotFoundError = OSError - itertools.filterfalse = itertools.ifilterfalse - - -@contextlib.contextmanager -def io_error_compat(): - try: - yield - except IOError as io_err: - # On Python 2, io.open raises IOError; transform to OSError for - # future compatibility. - os_err = OSError(*io_err.args) - os_err.filename = getattr(io_err, 'filename', None) - raise os_err - -############################################################################## - __all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] @@ -165,7 +128,7 @@ def load(param): pattern. """ return ( - matchers.Pattern(param) if isinstance(param, string_types) + matchers.Pattern(param) if isinstance(param, str) else param if param is not None else matchers.Null() ) @@ -209,7 +172,7 @@ class CaseInsensitive(Pattern): normcase = staticmethod(ntpath.normcase) -class Path(text_type): +class Path(str): """ Represents a filesystem path. @@ -239,8 +202,6 @@ def __init__(self, other=''): @simple_cache def using_module(cls, module): subclass_name = cls.__name__ + '_' + module.__name__ - if PY2: - subclass_name = str(subclass_name) bases = (cls,) ns = {'module': module} return type(subclass_name, bases, ns) @@ -266,7 +227,7 @@ def __add__(self, more): return NotImplemented def __radd__(self, other): - if not isinstance(other, string_types): + if not isinstance(other, str): return NotImplemented return self._next_class(other.__add__(self)) @@ -315,7 +276,7 @@ def getcwd(cls): .. seealso:: :func:`os.getcwdu` """ - return cls(getcwdu()) + return cls(os.getcwd()) # # --- Operations on Path strings. @@ -749,8 +710,7 @@ def open(self, *args, **kwargs): Keyword arguments work as in :func:`io.open`. If the file cannot be opened, an :class:`~exceptions.OSError` is raised. """ - with io_error_compat(): - return io.open(self, *args, **kwargs) + return io.open(self, *args, **kwargs) def bytes(self): """ Open this file, read all bytes, return them as a string. """ @@ -864,7 +824,7 @@ def write_text(self, text, encoding=None, errors='strict', conversion. """ - if isinstance(text, text_type): + if isinstance(text, str): if linesep is not None: text = U_NEWLINE.sub(linesep, text) text = text.encode(encoding or sys.getdefaultencoding(), errors) @@ -932,7 +892,7 @@ def write_lines(self, lines, encoding=None, errors='strict', """ with self.open('ab' if append else 'wb') as f: for line in lines: - isUnicode = isinstance(line, text_type) + isUnicode = isinstance(line, str) if linesep is not None: pattern = U_NL_END if isUnicode else NL_END line = pattern.sub('', line) + linesep @@ -1158,7 +1118,7 @@ def chmod(self, mode): .. seealso:: :func:`os.chmod` """ - if isinstance(mode, string_types): + if isinstance(mode, str): mask = _multi_permission_mask(mode) mode = mask(self.stat().st_mode) os.chmod(self, mode) @@ -1171,9 +1131,9 @@ def chown(self, uid=-1, gid=-1): .. seealso:: :func:`os.chown` """ if hasattr(os, 'chown'): - if 'pwd' in globals() and isinstance(uid, string_types): + if 'pwd' in globals() and isinstance(uid, str): uid = pwd.getpwnam(uid).pw_uid - if 'grp' in globals() and isinstance(gid, string_types): + if 'grp' in globals() and isinstance(gid, str): gid = grp.getgrnam(gid).gr_gid os.chown(self, uid, gid) else: @@ -1278,11 +1238,8 @@ def remove(self): def remove_p(self): """ Like :meth:`remove`, but does not raise an exception if the file does not exist. """ - try: + with contextlib.suppress(FileNotFoundError): self.unlink() - except FileNotFoundError as exc: - if PY2 and exc.errno != errno.ENOENT: - raise return self def unlink(self): @@ -1361,12 +1318,8 @@ def readlinkabs(self): def rmtree_p(self): """ Like :meth:`rmtree`, but does not raise an exception if the directory does not exist. """ - try: + with contextlib.suppress(FileNotFoundError): self.rmtree() - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.ENOENT: - raise return self def chdir(self): @@ -1377,7 +1330,7 @@ def chdir(self): def merge_tree( self, dst, symlinks=False, - # * + *, update=False, copy_function=shutil.copy2, ignore=lambda dir, contents: []): @@ -1622,8 +1575,6 @@ class Multi: @classmethod def for_class(cls, path_cls): name = 'Multi' + path_cls.__name__ - if PY2: - name = str(name) return type(name, (cls, path_cls), {}) @classmethod @@ -1784,38 +1735,3 @@ def __init__(self, *args, **kwargs): stacklevel=2, ) super(FastPath, self).__init__(*args, **kwargs) - - -def patch_for_linux_python2(): - """ - As reported in #130, when Linux users create filenames - not in the file system encoding, it creates problems on - Python 2. This function attempts to patch the os module - to make it behave more like that on Python 3. - """ - if not PY2 or platform.system() != 'Linux': - return - - try: - import backports.os - except ImportError: - return - - class OS: - """ - The proxy to the os module - """ - def __init__(self, wrapped): - self._orig = wrapped - - def __getattr__(self, name): - return getattr(self._orig, name) - - def listdir(self, *args, **kwargs): - items = self._orig.listdir(*args, **kwargs) - return list(map(backports.os.fsdecode, items)) - - globals().update(os=OS(os)) - - -patch_for_linux_python2() diff --git a/setup.cfg b/setup.cfg index a56b4943..c4c460cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Operating System :: OS Independent Topic :: Software Development :: Libraries :: Python Modules @@ -23,10 +22,9 @@ classifiers = [options] py_modules = path -python_requires = >=2.7,!=3.1,!=3.2,!=3.3 +python_requires = >=3.5 install_requires = importlib_metadata>=0.5 - backports.os; python_version=="2.7" and sys_platform=="linux2" setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] diff --git a/test_path.py b/test_path.py index 2a7ddb8f..f2640c1a 100644 --- a/test_path.py +++ b/test_path.py @@ -237,9 +237,6 @@ def test_joinpath_returns_same_type(self): class TestPerformance: - @pytest.mark.skipif( - path.PY2, - reason="Tests fail frequently on Python 2; see #153") def test_import_time(self, monkeypatch): """ Import of path.py should take less than 100ms. @@ -417,7 +414,7 @@ def test_listing(self, tmpdir): reason="macOS disallows invalid encodings", ) @pytest.mark.xfail( - platform.system() == 'Windows' and path.PY3, + platform.system() == 'Windows', reason="Can't write latin characters. See #133", ) def test_listdir_other_encoding(self, tmpdir): @@ -1236,7 +1233,6 @@ def test_iteration(self): assert path == input -@pytest.mark.xfail('path.PY2', reason="Python 2 has no __future__") def test_no_dependencies(): """ Path.py guarantees that the path module can be From 522294c089b3a637ec93371e09f2b2b69bb39407 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Apr 2019 02:37:11 -0400 Subject: [PATCH 229/835] Rely on more OSError subclasses --- path.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/path.py b/path.py index 6bdb8b62..1678fc7a 100644 --- a/path.py +++ b/path.py @@ -1162,12 +1162,8 @@ def mkdir(self, mode=0o777): def mkdir_p(self, mode=0o777): """ Like :meth:`mkdir`, but does not raise an exception if the directory already exists. """ - try: + with contextlib.suppress(FileExistsError): self.mkdir(mode) - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.EEXIST: - raise return self def makedirs(self, mode=0o777): @@ -1178,12 +1174,8 @@ def makedirs(self, mode=0o777): def makedirs_p(self, mode=0o777): """ Like :meth:`makedirs`, but does not raise an exception if the directory already exists. """ - try: + with contextlib.suppress(FileExistsError): self.makedirs(mode) - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.EEXIST: - raise return self def rmdir(self): @@ -1195,11 +1187,11 @@ def rmdir_p(self): """ Like :meth:`rmdir`, but does not raise an exception if the directory is not empty or does not exist. """ try: - self.rmdir() + with contextlib.suppress(FileNotFoundError, FileExistsError): + self.rmdir() except OSError: _, e, _ = sys.exc_info() - bypass_codes = errno.ENOTEMPTY, errno.EEXIST, errno.ENOENT - if e.errno not in bypass_codes: + if e.errno != errno.ENOTEMPTY: raise return self @@ -1212,10 +1204,11 @@ def removedirs_p(self): """ Like :meth:`removedirs`, but does not raise an exception if the directory is not empty or does not exist. """ try: - self.removedirs() + with contextlib.suppress(FileExistsError): + self.removedirs() except OSError: _, e, _ = sys.exc_info() - if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: + if e.errno != errno.ENOTEMPTY: raise return self From f27ba8a77974f1fad2ba2fd97b1ac7db987446c0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Apr 2019 02:45:36 -0400 Subject: [PATCH 230/835] Wrap the final OSError with a subclass for DirectoryNotEmpty --- path.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/path.py b/path.py index 1678fc7a..f026372c 100644 --- a/path.py +++ b/path.py @@ -1186,13 +1186,10 @@ def rmdir(self): def rmdir_p(self): """ Like :meth:`rmdir`, but does not raise an exception if the directory is not empty or does not exist. """ - try: - with contextlib.suppress(FileNotFoundError, FileExistsError): + suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty + with contextlib.suppress(suppressed): + with DirectoryNotEmpty.translate(): self.rmdir() - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.ENOTEMPTY: - raise return self def removedirs(self): @@ -1203,13 +1200,9 @@ def removedirs(self): def removedirs_p(self): """ Like :meth:`removedirs`, but does not raise an exception if the directory is not empty or does not exist. """ - try: - with contextlib.suppress(FileExistsError): + with contextlib.suppress(FileExistsError, DirectoryNotEmpty): + with DirectoryNotEmpty.translate(): self.removedirs() - except OSError: - _, e, _ = sys.exc_info() - if e.errno != errno.ENOTEMPTY: - raise return self # --- Modifying operations on files @@ -1509,6 +1502,19 @@ def special(cls): return functools.partial(SpecialResolver, cls) +class DirectoryNotEmpty(OSError): + @staticmethod + @contextlib.contextmanager + def translate(): + try: + yield + except OSError: + _, e, _ = sys.exc_info() + if e.errno == errno.ENOTEMPTY: + e.__class__ = DirectoryNotEmpty + raise + + def only_newer(copy_func): """ Wrap a copy function (like shutil.copy2) to return From 24cb70749f5c732ece25882e65eb41e283045cdb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2019 08:56:48 -0400 Subject: [PATCH 231/835] Update changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9496806f..095f08d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,7 @@ --- - #148: Dropped support for Python 2.7 and 3.4. +- Moved 'path' into a package. 11.5.2 ------ From eb9d686fcc11e2088927ede171018f4252e4778f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2019 08:59:07 -0400 Subject: [PATCH 232/835] Remove reference to Python 2 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 424d3e77..7a8cf905 100644 --- a/README.rst +++ b/README.rst @@ -72,7 +72,7 @@ But the implementation and the usage quickly diverge, and ``path.py`` has several advantages over ``pathlib``: - ``path.py`` implements ``Path`` objects as a subclass of - ``str`` (unicode on Python 2), and as a result these ``Path`` + ``str``, and as a result these ``Path`` objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one must first cast values to strings before passing them to From 20553e62f097de02975045c8dd7d3749f717dff3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Apr 2019 09:01:57 -0400 Subject: [PATCH 233/835] Update testing docs to direct user to run tox. --- README.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 7a8cf905..030a1916 100644 --- a/README.rst +++ b/README.rst @@ -119,13 +119,12 @@ the repository:: Testing ======= -Tests are continuously run by Travis-CI: |BuildStatus|_ +Tests are invoked with `tox `_. After +having installed tox, simply invoke ``tox`` in a checkout of the repo +to invoke the tests. -.. |BuildStatus| image:: https://secure.travis-ci.org/jaraco/path.py.png -.. _BuildStatus: http://travis-ci.org/jaraco/path.py - -To run the tests, refer to the ``.travis.yml`` file for the steps run on the -Travis-CI hosts. +Tests are also run in continuous integration. See the badges above +for links to the CI runs. Releasing ========= From b77a7a5e2b929e40c6e73ff88fe2109a1aa60146 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 02:06:58 -0400 Subject: [PATCH 234/835] Remove 'universal' wheel, now that Python 2 is no longer supported. Fixes #166. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index c4c460cd..da5267f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE name = path.py From 380d420cf639ce63718e574655f9a44816b0b524 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 02:07:43 -0400 Subject: [PATCH 235/835] Update changelog. Ref #166. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9496806f..0fe7d8b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +12.0.1 +------ + +- #166: Removed 'universal' wheel support. + 12.0 --- From e06bd7e39ef93e436a4cd0939ac8e395300f3ba2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 03:21:22 -0400 Subject: [PATCH 236/835] Remove FastPath and CaseInsensitivePattern, no longer needed. --- path.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/path.py b/path.py index a1599aee..96103c83 100644 --- a/path.py +++ b/path.py @@ -1788,26 +1788,6 @@ def _permission_mask(mode): return functools.partial(op_map[op], mask) -class CaseInsensitivePattern(matchers.CaseInsensitive): - def __init__(self, value): - warnings.warn( - "Use matchers.CaseInsensitive instead", - DeprecationWarning, - stacklevel=2, - ) - super(CaseInsensitivePattern, self).__init__(value) - - -class FastPath(Path): - def __init__(self, *args, **kwargs): - warnings.warn( - "Use Path, as FastPath no longer holds any advantage", - DeprecationWarning, - stacklevel=2, - ) - super(FastPath, self).__init__(*args, **kwargs) - - def patch_for_linux_python2(): """ As reported in #130, when Linux users create filenames From 2c7862e67a57fa0bb20d63a3762cfb34cb46cc59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 03:29:03 -0400 Subject: [PATCH 237/835] Remove deprecated 'update' parameter on merge_tree. --- path.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/path.py b/path.py index fbc26233..a22ac7cd 100644 --- a/path.py +++ b/path.py @@ -1415,7 +1415,6 @@ def chdir(self): def merge_tree( self, dst, symlinks=False, # * - update=False, copy_function=shutil.copy2, ignore=lambda dir, contents: []): """ @@ -1434,15 +1433,6 @@ def merge_tree( dst = self._next_class(dst) dst.makedirs_p() - if update: - warnings.warn( - "Update is deprecated; " - "use copy_function=only_newer(shutil.copy2)", - DeprecationWarning, - stacklevel=2, - ) - copy_function = only_newer(copy_function) - sources = self.listdir() _ignored = ignore(self, [item.name for item in sources]) @@ -1458,7 +1448,6 @@ def ignored(item): source.merge_tree( dest, symlinks=symlinks, - update=update, copy_function=copy_function, ignore=ignore, ) From 044ae78ce2ce7acd1680f13668cb6e80d148ff67 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 03:31:07 -0400 Subject: [PATCH 238/835] Remove deprecated 'namebase' property. --- path.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/path.py b/path.py index fc3d6de6..34920a14 100644 --- a/path.py +++ b/path.py @@ -325,11 +325,6 @@ def stem(self): base, ext = self.module.splitext(self.name) return base - @property - def namebase(self): - warnings.warn("Use .stem instead of .namebase", DeprecationWarning) - return self.stem - @property def ext(self): """ The file extension, for example ``'.py'``. """ From bb4c6091319fc3d33d4aebd15da483bb90acdbc9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 17 Apr 2019 15:22:26 -0400 Subject: [PATCH 239/835] Enable tox-pip-extensions ext_venv_update if available. Fixes jaraco/skeleton#6 --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 70b0be7a..5ce2047b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = python minversion = 2.4 +# https://github.com/jaraco/skeleton/issues/6 +tox_pip_extensions_ext_venv_update = true [testenv] deps = From 5bd3e6069e32cc94725fa389758e055126f3cdc5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2019 09:18:44 -0400 Subject: [PATCH 240/835] Rely on tox 3.2 and pip 10 or later for all builds --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5ce2047b..78161a56 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = python -minversion = 2.4 +minversion = 3.2 +requires = + pip >= 10 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true From 51f138939c98a4f616c702bc2f080504395fbbd6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2019 11:17:47 -0400 Subject: [PATCH 241/835] It adds no value to add a pip requirement for the tox install --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 78161a56..8a3bf67c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist = python minversion = 3.2 -requires = - pip >= 10 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true From 123b0b20d6e0bc9ffd00d7fb8c2e1a3ceee7475a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2019 13:20:53 -0400 Subject: [PATCH 242/835] Pin to pip 19.0 for now for pypa/pip#6434. --- pin-pip.py | 20 ++++++++++++++++++++ tox.ini | 1 + 2 files changed, 21 insertions(+) create mode 100644 pin-pip.py diff --git a/pin-pip.py b/pin-pip.py new file mode 100644 index 00000000..4cf0383c --- /dev/null +++ b/pin-pip.py @@ -0,0 +1,20 @@ +""" +Downgrade to pip 19.0 before installing requirements, working +around limitations introduced in 19.1 (ref +https://github.com/pypa/pip/issues/6434) +""" + +import sys +import subprocess +import shlex + + +def main(): + subprocess.check_call(shlex.split( + 'python -m pip install pip<19.1' + )) + subprocess.check_call(shlex.split( + 'python -m pip install') + sys.argv[1:]) + + +__name__ == '__main__' and main() diff --git a/tox.ini b/tox.ini index 8a3bf67c..3d1ae59d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ minversion = 3.2 tox_pip_extensions_ext_venv_update = true [testenv] +install_command = python pin-pip.py {opts} {packages} deps = setuptools>=31.0.1 commands = From 4186b77c3f225a5845ac9072b167427c91f1d6fd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 9 May 2019 08:55:31 -0400 Subject: [PATCH 243/835] Revert "Pin to pip 19.0 for now for pypa/pip#6434." This reverts commit 123b0b20d6e0bc9ffd00d7fb8c2e1a3ceee7475a. --- pin-pip.py | 20 -------------------- tox.ini | 1 - 2 files changed, 21 deletions(-) delete mode 100644 pin-pip.py diff --git a/pin-pip.py b/pin-pip.py deleted file mode 100644 index 4cf0383c..00000000 --- a/pin-pip.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Downgrade to pip 19.0 before installing requirements, working -around limitations introduced in 19.1 (ref -https://github.com/pypa/pip/issues/6434) -""" - -import sys -import subprocess -import shlex - - -def main(): - subprocess.check_call(shlex.split( - 'python -m pip install pip<19.1' - )) - subprocess.check_call(shlex.split( - 'python -m pip install') + sys.argv[1:]) - - -__name__ == '__main__' and main() diff --git a/tox.ini b/tox.ini index 3d1ae59d..8a3bf67c 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ minversion = 3.2 tox_pip_extensions_ext_venv_update = true [testenv] -install_command = python pin-pip.py {opts} {packages} deps = setuptools>=31.0.1 commands = From 4399038f2eab21f942a5462e0f5b1351e6203873 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 May 2019 19:45:15 -0400 Subject: [PATCH 244/835] Only install and invoke pytest-black on Python 3 --- pytest.ini | 2 +- setup.cfg | 2 +- tox.ini | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 10681adf..9b3c1ecd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 --black +addopts=--doctest-modules --flake8 doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 diff --git a/setup.cfg b/setup.cfg index a3eb3c94..235303f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ testing = pytest >= 3.5, !=3.7.3 pytest-checkdocs pytest-flake8 - pytest-black + pytest-black; python_version >= "3" # local diff --git a/tox.ini b/tox.ini index 8a3bf67c..8fa79660 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,8 @@ tox_pip_extensions_ext_venv_update = true deps = setuptools>=31.0.1 commands = - pytest {posargs} + !py27: pytest --black {posargs} + py27: pytest {posargs} usedevelop = True extras = testing From d4c65e6784e783549bfe5bba1ccbc7be76eb41ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 May 2019 21:21:57 -0400 Subject: [PATCH 245/835] Use pytest-black-multipy to enable simple support for pytest-black where available. Ref pytest-dev/pytest#5272. --- pytest.ini | 2 +- setup.cfg | 2 +- tox.ini | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pytest.ini b/pytest.ini index 9b3c1ecd..10681adf 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 +addopts=--doctest-modules --flake8 --black doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 diff --git a/setup.cfg b/setup.cfg index 235303f0..9345869b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ testing = pytest >= 3.5, !=3.7.3 pytest-checkdocs pytest-flake8 - pytest-black; python_version >= "3" + pytest-black-multipy # local diff --git a/tox.ini b/tox.ini index 8fa79660..8a3bf67c 100644 --- a/tox.ini +++ b/tox.ini @@ -8,8 +8,7 @@ tox_pip_extensions_ext_venv_update = true deps = setuptools>=31.0.1 commands = - !py27: pytest --black {posargs} - py27: pytest {posargs} + pytest {posargs} usedevelop = True extras = testing From 79733f08c43f9b2e0fd1830b37311fa52a16537c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 May 2019 10:21:15 -0400 Subject: [PATCH 246/835] Update skeleton documentation to reflect black adoption. --- skeleton.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skeleton.md b/skeleton.md index 09485cca..72494070 100644 --- a/skeleton.md +++ b/skeleton.md @@ -50,7 +50,8 @@ The features/techniques employed by the skeleton include: - setuptools declarative configuration using setup.cfg - tox for running tests - A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out -- A CHANGES.rst file intended for publishing release notes about the project. +- A CHANGES.rst file intended for publishing release notes about the project +- Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) ## Packaging Conventions @@ -97,8 +98,8 @@ A pytest.ini is included to define common options around running tests. In parti Relies a .flake8 file to correct some default behaviors: -- allow tabs for indentation (legacy for jaraco projects) -- disable mutually incompatible rules W503 and W504. +- disable mutually incompatible rules W503 and W504 +- support for black format ## Continuous Integration From e8db26a129378279833620da952a1ecc6cef937b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 May 2019 16:17:33 -0400 Subject: [PATCH 247/835] Add support for automatic publishing of release notes --- .travis.yml | 6 ++++++ tox.ini | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .travis.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..13f58071 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +jobs: + include: + - stage: deploy + env: + # TIDELIFT_TOKEN + - secure: ... diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..8f419798 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[testenv:release] +passenv = + TIDELIFT_TOKEN +deps = + jaraco.tidelift +commands = + python -m jaraco.tidelift.publish-release-notes From 534678f19edd76e6f93ad31900d9a92d00ef25fd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 May 2019 16:37:45 -0400 Subject: [PATCH 248/835] Use technique for environment passing matching that found in jaraco/skeleton --- .travis.yml | 1 + tox.ini | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13f58071..a9afa23f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,4 @@ jobs: env: # TIDELIFT_TOKEN - secure: ... + TOX_TESTENV_PASSENV="TIDELIFT_TOKEN" diff --git a/tox.ini b/tox.ini index 8f419798..7f9b6c1e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,4 @@ [testenv:release] -passenv = - TIDELIFT_TOKEN deps = jaraco.tidelift commands = From 2ab127d2bc47ffd747afe3059b3a5b08254a5415 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 May 2019 08:09:02 -0400 Subject: [PATCH 249/835] Rely on twine 1.13 or later --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 8a3bf67c..5f1d1b7f 100644 --- a/tox.ini +++ b/tox.ini @@ -24,8 +24,7 @@ commands = skip_install = True deps = pep517>=0.5 - # workaround for https://github.com/pypa/twine/issues/423 - git+https://github.com/pypa/twine + twine>=1.13 path.py commands = python -c "import path; path.Path('dist').rmtree_p()" From 054ea7dbbaabf257e7c3c6276d889cc178a19340 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jun 2019 16:38:07 -0400 Subject: [PATCH 250/835] Upgrade tox and virtualenv to ensure that environments get recent pips --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 2b7808f9..f35aa27d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ cache: - '%LOCALAPPDATA%\pip\Cache' test_script: - - "python -m pip install tox tox-venv" + - "python -m pip install -U tox tox-venv virtualenv" - "tox" version: '{build}' From 02eb16f0eb2cdc0015972ce963357aaa1cd0b84b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Jul 2019 12:57:18 -0400 Subject: [PATCH 251/835] Correct usage of exception resolution from exception info context. Fixes #167. --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index f2640c1a..8ef2bd6b 100644 --- a/test_path.py +++ b/test_path.py @@ -1097,7 +1097,7 @@ def test_exception_in_context(self, tmpdir): with doc.in_place() as (reader, writer): writer.write(self.alternate_content) raise RuntimeError("some error") - assert "some error" in str(exc) + assert "some error" in str(exc.value) with doc.open() as stream: data = stream.read() assert 'Lorem' not in data From 91a5c22fc8173ec75a637d776e60a90fcae4068c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Jul 2019 14:14:05 -0400 Subject: [PATCH 252/835] Compare the import of path against a baseline python startup. Potentially fixes #159. --- test_path.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test_path.py b/test_path.py index 8ef2bd6b..ed521bfd 100644 --- a/test_path.py +++ b/test_path.py @@ -237,23 +237,29 @@ def test_joinpath_returns_same_type(self): class TestPerformance: + @staticmethod + def get_command_time(cmd): + args = [ + sys.executable, + '-m', 'timeit', + '-n', '1', + '-r', '1', + '-u', 'usec', + ] + [cmd] + res = subprocess.check_output(args, universal_newlines=True) + dur = re.search(r'(\d+) usec per loop', res).group(1) + return datetime.timedelta(microseconds=int(dur)) + def test_import_time(self, monkeypatch): """ - Import of path.py should take less than 100ms. + Import of path.py should take less than some limit. Run tests in a subprocess to isolate from test suite overhead. """ - cmd = [ - sys.executable, - '-m', 'timeit', - '-n', '1', - '-r', '1', - 'import path', - ] - res = subprocess.check_output(cmd, universal_newlines=True) - dur = re.search(r'(\d+) msec per loop', res).group(1) - limit = datetime.timedelta(milliseconds=100) - duration = datetime.timedelta(milliseconds=int(dur)) + limit = datetime.timedelta(milliseconds=20) + baseline = self.get_command_time('pass') + measure = self.get_command_time('import path') + duration = measure - baseline assert duration < limit From 9c67e539d08f37b4331797375541c18c860020ca Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Oct 2018 22:14:00 +0200 Subject: [PATCH 253/835] Initial work on using generator.send to control traversal (doesn't work). --- path/__init__.py | 17 ++++++++++++++--- test_path.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8fe450b5..1d9db196 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -119,6 +119,12 @@ def __get__(self, instance, owner): ) +def skip_links(walker): + for item in walker: + walker.send(lambda: (item.isdir() and not item.islink())) + yield item + + class Path(str): """ Represents a filesystem path. @@ -561,10 +567,15 @@ def ignore(msg): return for child in childList: + traverse = None if match(child): - yield child + traverse = (yield child) + if traverse is not None: + # caller sent a custom traversal; respond + yield None + traverse = traverse or child.isdir try: - isdir = child.isdir() + do_traverse = traverse() except Exception: exc = sys.exc_info()[1] tmpl = "Unable to access '%(child)s': %(exc)s" @@ -572,7 +583,7 @@ def ignore(msg): errors(msg) isdir = False - if isdir: + if do_traverse: for item in child.walk(errors=errors, match=match): yield item diff --git a/test_path.py b/test_path.py index ed521bfd..e4758aa8 100644 --- a/test_path.py +++ b/test_path.py @@ -263,6 +263,20 @@ def test_import_time(self, monkeypatch): assert duration < limit +class TestSymbolicLinksWalk: + def test_skip_symlinks(self, tmpdir): + root = Path(tmpdir) + sub = root / 'subdir' + sub.mkdir() + sub.symlink(root / 'link') + (sub / 'file').touch() + assert len(list(root.walk())) == 4 + + from path import skip_links + + assert len(list(skip_links(root.walk()))) == 2 + + class TestSelfReturn: """ Some methods don't necessarily return any value (e.g. makedirs, From da7f134afb24e91f34bc4f138831458b6df09bcc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 04:03:44 -0400 Subject: [PATCH 254/835] Fix traversal and skip_links implementation --- path/__init__.py | 14 +++++++++----- test_path.py | 5 +---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1d9db196..2123396f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -120,10 +120,17 @@ def __get__(self, instance, owner): def skip_links(walker): - for item in walker: - walker.send(lambda: (item.isdir() and not item.islink())) + traverse = None + while True: + try: + item = walker.send(traverse) + except StopIteration: + return yield item + def traverse(): + return item.isdir() and not item.islink() + class Path(str): """ @@ -570,9 +577,6 @@ def ignore(msg): traverse = None if match(child): traverse = (yield child) - if traverse is not None: - # caller sent a custom traversal; respond - yield None traverse = traverse or child.isdir try: do_traverse = traverse() diff --git a/test_path.py b/test_path.py index e4758aa8..fad1b05d 100644 --- a/test_path.py +++ b/test_path.py @@ -271,10 +271,7 @@ def test_skip_symlinks(self, tmpdir): sub.symlink(root / 'link') (sub / 'file').touch() assert len(list(root.walk())) == 4 - - from path import skip_links - - assert len(list(skip_links(root.walk()))) == 2 + assert len(list(path.skip_links(root.walk()))) == 3 class TestSelfReturn: From 86bc246f303e25f1b474e60619e1a731155b34f3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 04:21:40 -0400 Subject: [PATCH 255/835] Implement the Traversal class as a generic wrapper of a follow function. --- path/__init__.py | 30 ++++++++++++++++++++---------- test_path.py | 5 ++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 2123396f..8c7bdb62 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -119,17 +119,27 @@ def __get__(self, instance, owner): ) -def skip_links(walker): - traverse = None - while True: - try: - item = walker.send(traverse) - except StopIteration: - return - yield item +class Traversal: + """ + Wrap a walk result to customize the traversal. + + `follow` is a function that takes an item and returns + True if that item should be followed and False otherwise. + """ + def __init__(self, walker, follow): + self.follow = follow + self.walker = walker + + def __iter__(self): + traverse = None + while True: + try: + item = self.walker.send(traverse) + except StopIteration: + return + yield item - def traverse(): - return item.isdir() and not item.islink() + traverse = functools.partial(self.follow, item) class Path(str): diff --git a/test_path.py b/test_path.py index fad1b05d..ecc75852 100644 --- a/test_path.py +++ b/test_path.py @@ -271,7 +271,10 @@ def test_skip_symlinks(self, tmpdir): sub.symlink(root / 'link') (sub / 'file').touch() assert len(list(root.walk())) == 4 - assert len(list(path.skip_links(root.walk()))) == 3 + + def skip_links(item): + return item.isdir() and not item.islink() + assert len(list(path.Traversal(root.walk(), skip_links))) == 3 class TestSelfReturn: From 085a83da442928465c72acc8bbd561611d56f93d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 13 Apr 2019 04:32:28 -0400 Subject: [PATCH 256/835] Let the Traversal class simply accept the function and then wrap the walker --- path/__init__.py | 7 +++---- test_path.py | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8c7bdb62..1dcbe62e 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -126,15 +126,14 @@ class Traversal: `follow` is a function that takes an item and returns True if that item should be followed and False otherwise. """ - def __init__(self, walker, follow): + def __init__(self, follow): self.follow = follow - self.walker = walker - def __iter__(self): + def __call__(self, walker): traverse = None while True: try: - item = self.walker.send(traverse) + item = walker.send(traverse) except StopIteration: return yield item diff --git a/test_path.py b/test_path.py index ecc75852..5e24b797 100644 --- a/test_path.py +++ b/test_path.py @@ -272,9 +272,10 @@ def test_skip_symlinks(self, tmpdir): (sub / 'file').touch() assert len(list(root.walk())) == 4 - def skip_links(item): - return item.isdir() and not item.islink() - assert len(list(path.Traversal(root.walk(), skip_links))) == 3 + skip_links = path.Traversal( + lambda item: item.isdir() and not item.islink(), + ) + assert len(list(skip_links(root.walk()))) == 3 class TestSelfReturn: From 20964edcfd5b35c6006bc6425a5587004ed76eec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2019 22:39:34 -0400 Subject: [PATCH 257/835] Define passenv in tox release section. Rely on __token__ for default username. --- .travis.yml | 5 ----- tox.ini | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17d02624..6ccac8f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,6 @@ jobs: if: tag IS present python: *latest_py3 before_script: skip - env: - - TWINE_USERNAME=jaraco - # TWINE_PASSWORD - - secure: ... - - TOX_TESTENV_PASSENV="TWINE_USERNAME TWINE_PASSWORD" script: tox -e release cache: pip diff --git a/tox.ini b/tox.ini index 5f1d1b7f..4f3341f8 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,10 @@ deps = pep517>=0.5 twine>=1.13 path.py +passenv = + TWINE_PASSWORD +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import path; path.Path('dist').rmtree_p()" python -m pep517.build . From 4baec898fdfef6da27653d21fdf223da10b13342 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2019 16:40:51 -0400 Subject: [PATCH 258/835] Update docs to reflect changes to deployment. --- skeleton.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/skeleton.md b/skeleton.md index 72494070..52b97f09 100644 --- a/skeleton.md +++ b/skeleton.md @@ -103,13 +103,7 @@ Relies a .flake8 file to correct some default behaviors: ## Continuous Integration -The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. In addition to running tests, an additional deploy stage is configured to automatically release tagged commits. The username and password for PyPI must be configured for each project using the `travis` command and only after the travis project is created. As releases are cut with [twine](https://pypi.org/project/twine), the two values are supplied through the `TWINE_USERNAME` and `TWINE_PASSWORD`. To configure the latter as a secret, run the following command: - -``` -echo "TWINE_PASSWORD={password}" | travis encrypt -``` - -Or disable it in the CI definition and configure it through the web UI. +The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. Features include: - test against Python 2 and 3 @@ -118,6 +112,14 @@ Features include: Also provided is a minimal template for running under Appveyor (Windows). +### Continuous Deployments + +In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax): + +``` +TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD +``` + ## Building Documentation Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. @@ -127,3 +129,9 @@ In addition to building the sphinx docs scaffolded in `docs/`, the docs build a ## Cutting releases By default, tagged commits are released through the continuous integration deploy stage. + +Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: + +``` +TWINE_PASSWORD={token} tox -e release +``` From fd540b6535f56647581df61333bac7eadc1309f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2019 20:01:38 -0400 Subject: [PATCH 259/835] Move Tidelift token into Travis configuration --- .travis.yml | 7 ------- tox.ini | 2 ++ 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a9afa23f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -jobs: - include: - - stage: deploy - env: - # TIDELIFT_TOKEN - - secure: ... - TOX_TESTENV_PASSENV="TIDELIFT_TOKEN" diff --git a/tox.ini b/tox.ini index 7f9b6c1e..35053514 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [testenv:release] deps = jaraco.tidelift +passenv = + TIDELIFT_TOKEN commands = python -m jaraco.tidelift.publish-release-notes From 211ff140a4bd82fcb4f01a5569cdc86e4badea8b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 16:36:40 +0100 Subject: [PATCH 260/835] Update badge URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 35762622..420bfb4f 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://tidelift.com/badges/github/GROUP/PROJECT +.. image:: https://tidelift.com/badges/package/pypi/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme Security Contact From 8f99a0c1b8ee2cb28a8bdb1811ef96da68636d1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 17:30:03 +0100 Subject: [PATCH 261/835] Add funding reference to project --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..230b556c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: pypi/PROJECT From 1c187ad0cf50fbc14626f63cb669a9ec5949012f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 17:45:58 +0100 Subject: [PATCH 262/835] List sidebars to avoid errors looking for template 't' --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3d109305..dbf962dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,4 +2,4 @@ # Custom sidebar templates, maps document names to template names. html_theme = 'alabaster' templates_path = ['_templates'] -html_sidebars = {'index': 'tidelift-sidebar.html'} +html_sidebars = {'index': ['tidelift-sidebar.html']} From 05a3c52b4d41690e0471a2e283cffb500dc0329a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2019 11:25:50 +0100 Subject: [PATCH 263/835] Python 3 only --- .travis.yml | 1 - appveyor.yml | 2 +- setup.cfg | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ccac8f2..8b607a65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ dist: xenial language: python python: -- 2.7 - 3.6 - &latest_py3 3.7 diff --git a/appveyor.yml b/appveyor.yml index f35aa27d..bfd57529 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ environment: matrix: - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python37-x64" install: # symlink python from a directory with a space diff --git a/setup.cfg b/setup.cfg index 9345869b..8dc6d4ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,13 +13,12 @@ classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 [options] packages = find: include_package_data = true -python_requires = >=2.7 +python_requires = >=3.6 install_requires = setup_requires = setuptools_scm >= 1.15.0 From a28efc59c12e16a02ec98659b660e5b5809af650 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Sep 2019 21:36:25 +0200 Subject: [PATCH 264/835] Enable coverage reporting on project --- .coveragerc | 2 ++ pytest.ini | 2 +- setup.cfg | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..896b501e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = .tox/* diff --git a/pytest.ini b/pytest.ini index 10681adf..a86fb660 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 --black +addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= ignore:Possible nested set::pycodestyle:113 diff --git a/setup.cfg b/setup.cfg index 9345869b..77df5051 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ testing = pytest-checkdocs pytest-flake8 pytest-black-multipy + pytest-cov # local From cc1a1c9be39ba29e90d6d9d8ab5d6d1768a50594 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 22 Sep 2019 14:25:32 +0200 Subject: [PATCH 265/835] Report the lines missing coverage --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 896b501e..45823064 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,5 @@ [run] omit = .tox/* + +[report] +show_missing = True From 9314eb458311dfd8981a6378b8498017c89ea2f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Sep 2019 22:46:03 -0400 Subject: [PATCH 266/835] Ensure that a late version of pip is installed without special versions of tox-venv. --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tox.ini b/tox.ini index 4f3341f8..ab6cd407 100644 --- a/tox.ini +++ b/tox.ini @@ -3,10 +3,16 @@ envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true +# ensure that a late version of pip is used even on tox-venv +requires = + tox-pip-version + tox-venv + [testenv] deps = setuptools>=31.0.1 +pip_version = pip commands = pytest {posargs} usedevelop = True From 1cfcc4214082bcb6bec9ea51ad91f15446488e9b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 20 Oct 2019 17:25:23 -0400 Subject: [PATCH 267/835] Disable tox-pip-version as it interacts badly with tox-venv causing tox to use the wrong Python version to install packages and run tests. Ref pglass/tox-pip-version#20 and tox-dev/tox-venv#40. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index ab6cd407..1d81b812 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,10 @@ envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true -# ensure that a late version of pip is used even on tox-venv +# Ensure that a late version of pip is used even on tox-venv. +# Disabled due to pglass/tox-pip-version#20. requires = - tox-pip-version +# tox-pip-version tox-venv From c169e5e50fd5f18dfe554d06bfe3940cc950b13e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Oct 2019 19:39:34 -0400 Subject: [PATCH 268/835] Bring back tox-pip-version now that pglass/tox-pip-version#20 is fixed. --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1d81b812..d267e16d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,8 @@ minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true # Ensure that a late version of pip is used even on tox-venv. -# Disabled due to pglass/tox-pip-version#20. requires = -# tox-pip-version + tox-pip-version>=0.0.6 tox-venv From 34ecf58479ad45fc6bfa5c8b476b719cf5720c14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 12:30:25 -0400 Subject: [PATCH 269/835] Test/release on Python 3.8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6ccac8f2..b7d8f3ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: python python: - 2.7 - 3.6 -- &latest_py3 3.7 +- &latest_py3 3.8 jobs: fast_finish: true From eaeb9ec2eb542a04948c6c9742fb90069f37de33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 13:29:45 -0400 Subject: [PATCH 270/835] Apply black to docs/conf.py --- docs/conf.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 77cef345..41b53557 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,19 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -extensions = [ - 'sphinx.ext.autodoc', - 'jaraco.packaging.sphinx', - 'rst.linker', -] +extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] master_doc = "index" link_files = { '../CHANGES.rst': dict( - using=dict( - GH='https://github.com', - ), + using=dict(GH='https://github.com'), replace=[ dict( pattern=r'(Issue #|\B#)(?P\d+)', @@ -28,5 +22,5 @@ url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), ], - ), + ) } From 174f0fd7cf349c277ade401ddb88dde530723053 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 13:36:04 -0400 Subject: [PATCH 271/835] Update black version and links --- .pre-commit-config.yaml | 4 ++-- README.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 922d9424..e16c59ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/ambv/black - rev: 18.9b0 +- repo: https://github.com/psf/black + rev: 19.3b0 hooks: - id: black diff --git a/README.rst b/README.rst index 7050da33..50eba567 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ :target: https://travis-ci.org/jaraco/skeleton .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/ambv/black + :target: https://github.com/psf/black :alt: Code style: Black .. .. image:: https://img.shields.io/appveyor/ci/jaraco/skeleton/master.svg From 03d825d44a003d08cb54708fd87fccd161806d3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Oct 2019 16:14:40 -0400 Subject: [PATCH 272/835] Expect flake8 3.6 or later and remove suppression of warnings from Flake8 prior to 3.6. --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index a86fb660..54f1b9aa 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,8 +3,6 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= - ignore:Possible nested set::pycodestyle:113 - ignore:Using or importing the ABCs::flake8:410 # workaround for https://sourceforge.net/p/docutils/bugs/348/ ignore:'U' mode is deprecated::docutils.io # workaround for https://gitlab.com/pycqa/flake8/issues/275 From 48d361a4146d6bbc52c96718ec83cf56410af73a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Oct 2019 16:30:21 -0400 Subject: [PATCH 273/835] Rely on pytest-checkdocs 1.2.3, eliminating workaround for docutils warning. --- pytest.ini | 2 -- setup.cfg | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 54f1b9aa..4ffa6f29 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,7 +3,5 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= - # workaround for https://sourceforge.net/p/docutils/bugs/348/ - ignore:'U' mode is deprecated::docutils.io # workaround for https://gitlab.com/pycqa/flake8/issues/275 ignore:You passed a bytestring as `filenames`.::flake8 diff --git a/setup.cfg b/setup.cfg index 77df5051..63b865ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ setup_requires = setuptools_scm >= 1.15.0 testing = # upstream pytest >= 3.5, !=3.7.3 - pytest-checkdocs + pytest-checkdocs >= 1.2.3 pytest-flake8 pytest-black-multipy pytest-cov From f10294a99395a64370695e43521175bb93e4b4f3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Nov 2019 11:54:25 -0400 Subject: [PATCH 274/835] Remove workaround for gitlab.com/pycqa/flake8/issues/275, apparently no longer necessary. --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 4ffa6f29..7b9b714f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,5 +3,3 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= - # workaround for https://gitlab.com/pycqa/flake8/issues/275 - ignore:You passed a bytestring as `filenames`.::flake8 From cfed4a65ce998608d35b609f79eab19f03bac200 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Nov 2019 22:08:07 -0500 Subject: [PATCH 275/835] Fade to black --- path/__init__.py | 166 +++++++++++++++++++++++++++++------------------ path/matchers.py | 7 +- test_path.py | 149 +++++++++++++++++++----------------------- 3 files changed, 175 insertions(+), 147 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8fe450b5..13a9a503 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -69,6 +69,7 @@ try: import importlib_metadata + __version__ = importlib_metadata.version('path.py') except Exception: __version__ = 'unknown' @@ -96,6 +97,7 @@ def wrapper(cls, module): return saved_results[module] saved_results[module] = func(cls, module) return saved_results[module] + return wrapper @@ -109,12 +111,14 @@ class multimethod(object): Acts like a classmethod when invoked from the class and like an instancemethod when invoked from the instance. """ + def __init__(self, func): self.func = func def __get__(self, instance, owner): return ( - functools.partial(self.func, owner) if instance is None + functools.partial(self.func, owner) + if instance is None else functools.partial(self.func, owner, instance) ) @@ -319,7 +323,9 @@ def drive(self): return self._next_class(drive) parent = property( - dirname, None, None, + dirname, + None, + None, """ This path's parent directory, as a new Path object. For example, @@ -327,17 +333,21 @@ def drive(self): Path('/usr/local/lib')`` .. seealso:: :meth:`dirname`, :func:`os.path.dirname` - """) + """, + ) name = property( - basename, None, None, + basename, + None, + None, """ The name of this file or directory without the full path. For example, ``Path('/usr/local/lib/libpython.so').name == 'libpython.so'`` .. seealso:: :meth:`basename`, :func:`os.path.basename` - """) + """, + ) def splitpath(self): """ p.splitpath() -> Return ``(p.parent, p.name)``. @@ -493,9 +503,7 @@ def listdir(self, match=None): .. seealso:: :meth:`files`, :meth:`dirs` """ match = matchers.load(match) - return list(filter(match, ( - self / child for child in os.listdir(self) - ))) + return list(filter(match, (self / child for child in os.listdir(self)))) def dirs(self, *args, **kwargs): """ D.dirs() -> List of this directory's subdirectories. @@ -535,6 +543,7 @@ def walk(self, match=None, errors='strict'): reports the error via :func:`warnings.warn()`), and ``'ignore'``. `errors` may also be an arbitrary callable taking a msg parameter. """ + class Handlers: def strict(msg): raise @@ -579,20 +588,12 @@ def ignore(msg): def walkdirs(self, *args, **kwargs): """ D.walkdirs() -> iterator over subdirs, recursively. """ - return ( - item - for item in self.walk(*args, **kwargs) - if item.isdir() - ) + return (item for item in self.walk(*args, **kwargs) if item.isdir()) def walkfiles(self, *args, **kwargs): """ D.walkfiles() -> iterator over files in D, recursively. """ - return ( - item - for item in self.walk(*args, **kwargs) - if item.isfile() - ) + return (item for item in self.walk(*args, **kwargs) if item.isfile()) def fnmatch(self, pattern, normcase=None): """ Return ``True`` if `self.name` matches the given `pattern`. @@ -706,8 +707,9 @@ def text(self, encoding=None, errors='strict'): with self.open(mode='r', encoding=encoding, errors=errors) as f: return U_NEWLINE.sub('\n', f.read()) - def write_text(self, text, encoding=None, errors='strict', - linesep=os.linesep, append=False): + def write_text( + self, text, encoding=None, errors='strict', linesep=os.linesep, append=False + ): r""" Write the given text to this file. The default behavior is to overwrite any existing file; @@ -799,8 +801,9 @@ def lines(self, encoding=None, errors='strict', retain=True): """ return self.text(encoding, errors).splitlines(retain) - def write_lines(self, lines, encoding=None, errors='strict', - linesep=os.linesep, append=False): + def write_lines( + self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False + ): r""" Write the given lines of text to this file. By default this overwrites any existing file at this path. @@ -844,8 +847,7 @@ def write_lines(self, lines, encoding=None, errors='strict', pattern = U_NL_END if isUnicode else NL_END line = pattern.sub('', line) + linesep if isUnicode: - line = line.encode( - encoding or sys.getdefaultencoding(), errors) + line = line.encode(encoding or sys.getdefaultencoding(), errors) f.write(line) def read_md5(self): @@ -929,46 +931,59 @@ def getatime(self): return self.module.getatime(self) atime = property( - getatime, None, None, + getatime, + None, + None, """ Last access time of the file. .. seealso:: :meth:`getatime`, :func:`os.path.getatime` - """) + """, + ) def getmtime(self): """ .. seealso:: :attr:`mtime`, :func:`os.path.getmtime` """ return self.module.getmtime(self) mtime = property( - getmtime, None, None, + getmtime, + None, + None, """ Last-modified time of the file. .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` - """) + """, + ) def getctime(self): """ .. seealso:: :attr:`ctime`, :func:`os.path.getctime` """ return self.module.getctime(self) ctime = property( - getctime, None, None, + getctime, + None, + None, """ Creation time of the file. .. seealso:: :meth:`getctime`, :func:`os.path.getctime` - """) + """, + ) def getsize(self): """ .. seealso:: :attr:`size`, :func:`os.path.getsize` """ return self.module.getsize(self) size = property( - getsize, None, None, + getsize, + None, + None, """ Size of the file, in bytes. .. seealso:: :meth:`getsize`, :func:`os.path.getsize` - """) + """, + ) if hasattr(os, 'access'): + def access(self, mode): """ Return ``True`` if current user has access to this path. @@ -1003,7 +1018,8 @@ def __get_owner_windows(self): .. seealso:: :attr:`owner` """ desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION) + self, win32security.OWNER_SECURITY_INFORMATION + ) sid = desc.GetSecurityDescriptorOwner() account, domain, typecode = win32security.LookupAccountSid(None, sid) return domain + '\\' + account @@ -1029,12 +1045,16 @@ def __get_owner_not_implemented(self): get_owner = __get_owner_not_implemented owner = property( - get_owner, None, None, + get_owner, + None, + None, """ Name of the owner of this file or directory. - .. seealso:: :meth:`get_owner`""") + .. seealso:: :meth:`get_owner`""", + ) if hasattr(os, 'statvfs'): + def statvfs(self): """ Perform a ``statvfs()`` system call on this path. @@ -1043,6 +1063,7 @@ def statvfs(self): return os.statvfs(self) if hasattr(os, 'pathconf'): + def pathconf(self, name): """ .. seealso:: :func:`os.pathconf` """ return os.pathconf(self, name) @@ -1189,6 +1210,7 @@ def unlink_p(self): # --- Links if hasattr(os, 'link'): + def link(self, newpath): """ Create a hard link at `newpath`, pointing to this file. @@ -1198,6 +1220,7 @@ def link(self, newpath): return self._next_class(newpath) if hasattr(os, 'symlink'): + def symlink(self, newlink=None): """ Create a symbolic link at `newlink`, pointing here. @@ -1212,6 +1235,7 @@ def symlink(self, newlink=None): return self._next_class(newlink) if hasattr(os, 'readlink'): + def readlink(self): """ Return the path to which this symbolic link points. @@ -1262,11 +1286,14 @@ def chdir(self): cd = chdir def merge_tree( - self, dst, symlinks=False, - *, - update=False, - copy_function=shutil.copy2, - ignore=lambda dir, contents: []): + self, + dst, + symlinks=False, + *, + update=False, + copy_function=shutil.copy2, + ignore=lambda dir, contents: [] + ): """ Copy entire contents of self to dst, overwriting existing contents in dst with those in self. @@ -1285,8 +1312,7 @@ def merge_tree( if update: warnings.warn( - "Update is deprecated; " - "use copy_function=only_newer(shutil.copy2)", + "Update is deprecated; " "use copy_function=only_newer(shutil.copy2)", DeprecationWarning, stacklevel=2, ) @@ -1320,11 +1346,13 @@ def ignored(item): # --- Special stuff from os if hasattr(os, 'chroot'): + def chroot(self): """ .. seealso:: :func:`os.chroot` """ os.chroot(self) if hasattr(os, 'startfile'): + def startfile(self): """ .. seealso:: :func:`os.startfile` """ os.startfile(self) @@ -1334,8 +1362,13 @@ def startfile(self): # http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ @contextlib.contextmanager def in_place( - self, mode='r', buffering=-1, encoding=None, errors=None, - newline=None, backup_extension=None, + self, + mode='r', + buffering=-1, + encoding=None, + errors=None, + newline=None, + backup_extension=None, ): """ A context in which a file may be re-written in-place with @@ -1376,15 +1409,22 @@ def in_place( pass os.rename(self, backup_fn) readable = io.open( - backup_fn, mode, buffering=buffering, - encoding=encoding, errors=errors, newline=newline, + backup_fn, + mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, ) try: perm = os.fstat(readable.fileno()).st_mode except OSError: writable = open( - self, 'w' + mode.replace('r', ''), - buffering=buffering, encoding=encoding, errors=errors, + self, + 'w' + mode.replace('r', ''), + buffering=buffering, + encoding=encoding, + errors=errors, newline=newline, ) else: @@ -1393,8 +1433,11 @@ def in_place( os_mode |= os.O_BINARY fd = os.open(self, os_mode, perm) writable = io.open( - fd, "w" + mode.replace('r', ''), - buffering=buffering, encoding=encoding, errors=errors, + fd, + "w" + mode.replace('r', ''), + buffering=buffering, + encoding=encoding, + errors=errors, newline=newline, ) try: @@ -1467,15 +1510,14 @@ def only_newer(copy_func): Wrap a copy function (like shutil.copy2) to return the dst if it's newer than the source. """ + @functools.wraps(copy_func) def wrapper(src, dst, *args, **kwargs): - is_newer_dst = ( - dst.exists() - and dst.getmtime() >= src.getmtime() - ) + is_newer_dst = dst.exists() and dst.getmtime() >= src.getmtime() if is_newer_dst: return dst return copy_func(src, dst, *args, **kwargs) + return wrapper @@ -1496,8 +1538,7 @@ def __init__(self, path_class, *args, **kwargs): not args and kwargs.setdefault('appname', None) vars(self).update( - path_class=path_class, - wrapper=appdirs.AppDirs(*args, **kwargs), + path_class=path_class, wrapper=appdirs.AppDirs(*args, **kwargs) ) def __getattr__(self, scope): @@ -1518,6 +1559,7 @@ class Multi: """ A mix-in for a Path which may contain multiple Path separated by pathsep. """ + @classmethod def for_class(cls, path_cls): name = 'Multi' + path_cls.__name__ @@ -1538,11 +1580,7 @@ def _next_class(cls): """ Multi-subclasses should use the parent class """ - return next( - class_ - for class_ in cls.__mro__ - if not issubclass(class_, Multi) - ) + return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi)) class TempDir(Path): @@ -1596,8 +1634,10 @@ def _multi_permission_mask(mode): >>> _multi_permission_mask('a=r,u+w')(0) == 0o644 True """ + def compose(f, g): return lambda *args, **kwargs: g(f(*args, **kwargs)) + return functools.reduce(compose, map(_permission_mask, mode.split(','))) @@ -1666,9 +1706,7 @@ def _permission_mask(mode): class CaseInsensitivePattern(matchers.CaseInsensitive): def __init__(self, value): warnings.warn( - "Use matchers.CaseInsensitive instead", - DeprecationWarning, - stacklevel=2, + "Use matchers.CaseInsensitive instead", DeprecationWarning, stacklevel=2 ) super(CaseInsensitivePattern, self).__init__(value) diff --git a/path/matchers.py b/path/matchers.py index c819ea51..0dc2727e 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -10,8 +10,10 @@ def load(param): pattern. """ return ( - Pattern(param) if isinstance(param, str) - else param if param is not None + Pattern(param) + if isinstance(param, str) + else param + if param is not None else Null() ) @@ -55,4 +57,5 @@ class CaseInsensitive(Pattern): from path import Path, matchers Path('.').files(matchers.CaseInsensitive('*.py')) """ + normcase = staticmethod(ntpath.normcase) diff --git a/test_path.py b/test_path.py index ed521bfd..8bd67575 100644 --- a/test_path.py +++ b/test_path.py @@ -64,10 +64,7 @@ def mac_version(target, comparator=operator.ge): """ current_ver = packaging.version.parse(platform.mac_ver()[0]) target_ver = packaging.version.parse(target) - return ( - platform.system() == 'Darwin' - and comparator(current_ver, target_ver) - ) + return platform.system() == 'Darwin' and comparator(current_ver, target_ver) class TestBasics: @@ -128,13 +125,7 @@ def test_string_compatibility(self): assert x == str('xyzzy') # sorting - items = [Path('fhj'), - Path('fgh'), - 'E', - Path('d'), - 'A', - Path('B'), - 'c'] + items = [Path('fhj'), Path('fgh'), 'E', Path('d'), 'A', Path('B'), 'c'] items.sort() assert items == ['A', 'B', 'E', 'c', 'd', 'fgh', 'fhj'] @@ -145,8 +136,10 @@ def test_string_compatibility(self): def test_properties(self): # Create sample path object. - f = p(nt='C:\\Program Files\\Python\\Lib\\xyzzy.py', - posix='/usr/local/python/lib/xyzzy.py') + f = p( + nt='C:\\Program Files\\Python\\Lib\\xyzzy.py', + posix='/usr/local/python/lib/xyzzy.py', + ) f = Path(f) # .parent @@ -239,13 +232,9 @@ def test_joinpath_returns_same_type(self): class TestPerformance: @staticmethod def get_command_time(cmd): - args = [ - sys.executable, - '-m', 'timeit', - '-n', '1', - '-r', '1', - '-u', 'usec', - ] + [cmd] + args = [sys.executable, '-m', 'timeit', '-n', '1', '-r', '1', '-u', 'usec'] + [ + cmd + ] res = subprocess.check_output(args, universal_newlines=True) dur = re.search(r'(\d+) usec per loop', res).group(1) return datetime.timedelta(microseconds=int(dur)) @@ -269,6 +258,7 @@ class TestSelfReturn: makedirs_p, rename, mkdir, touch, chroot). These methods should return self anyhow to allow methods to be chained. """ + def test_makedirs_p(self, tmpdir): """ Path('foo').makedirs_p() == Path('foo') @@ -304,6 +294,7 @@ class TestScratchDir: """ Tests that run in a temporary directory (does not test TempDir class) """ + def test_context_manager(self, tmpdir): """Can be used as context manager for chdir.""" d = Path(tmpdir) @@ -363,7 +354,8 @@ def test_touch(self, tmpdir): else: assert ( # ctime is unchanged - ct == ct2 or + ct == ct2 + or # ctime is approximately the mtime ct2 == pytest.approx(f.mtime, 0.001) ) @@ -415,13 +407,9 @@ def test_listing(self, tmpdir): except Exception: pass + @pytest.mark.xfail(mac_version('10.13'), reason="macOS disallows invalid encodings") @pytest.mark.xfail( - mac_version('10.13'), - reason="macOS disallows invalid encodings", - ) - @pytest.mark.xfail( - platform.system() == 'Windows', - reason="Can't write latin characters. See #133", + platform.system() == 'Windows', reason="Can't write latin characters. See #133" ) def test_listdir_other_encoding(self, tmpdir): """ @@ -538,9 +526,8 @@ def test_shutil(self, tmpdir): assert testC.isdir() self.assertSetsEqual( testC.listdir(), - [testC / testCopy.name, - testC / testFile.name, - testCopyOfLink]) + [testC / testCopy.name, testC / testFile.name, testCopyOfLink], + ) assert not testCopyOfLink.islink() # Clean up for another try. @@ -552,9 +539,8 @@ def test_shutil(self, tmpdir): assert testC.isdir() self.assertSetsEqual( testC.listdir(), - [testC / testCopy.name, - testC / testFile.name, - testCopyOfLink]) + [testC / testCopy.name, testC / testFile.name, testCopyOfLink], + ) if hasattr(os, 'symlink'): assert testCopyOfLink.islink() assert testCopyOfLink.readlink() == testFile @@ -581,11 +567,10 @@ def test_patterns(self, tmpdir): self.assertList(d.listdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) self.assertList(d.files('*.tmp'), [d / 'x.tmp']) self.assertList(d.dirs('*.tmp'), [d / 'xdir.tmp']) - self.assertList(d.walk(), [e for e in dirs - if e != d] + [e / n for e in dirs - for n in names]) - self.assertList(d.walk('*.tmp'), - [e / 'x.tmp' for e in dirs] + [d / 'xdir.tmp']) + self.assertList( + d.walk(), [e for e in dirs if e != d] + [e / n for e in dirs for n in names] + ) + self.assertList(d.walk('*.tmp'), [e / 'x.tmp' for e in dirs] + [d / 'xdir.tmp']) self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) @@ -621,21 +606,24 @@ def test(enc): ('\u0d0a\u0a0d\u0d15\u0a15\x85'), ('\u0d0a\u0a0d\u0d15\u0a15\u2028'), ('\r'), - ('hanging')] + ('hanging'), + ] expectedLines = [ ('Hello world\n'), ('\u0d0a\u0a0d\u0d15\u0a15\n'), ('\u0d0a\u0a0d\u0d15\u0a15\n'), ('\u0d0a\u0a0d\u0d15\u0a15\n'), ('\n'), - ('hanging')] + ('hanging'), + ] expectedLines2 = [ ('Hello world'), ('\u0d0a\u0a0d\u0d15\u0a15'), ('\u0d0a\u0a0d\u0d15\u0a15'), ('\u0d0a\u0a0d\u0d15\u0a15'), (''), - ('hanging')] + ('hanging'), + ] # write bytes manually to file f = codecs.open(p, 'w', enc) @@ -663,8 +651,7 @@ def test(enc): p.write_text(cleanNoHanging, enc) p.write_text(cleanNoHanging, enc, append=True) # Check the result. - expectedBytes = 2 * cleanNoHanging.replace('\n', - os.linesep).encode(enc) + expectedBytes = 2 * cleanNoHanging.replace('\n', os.linesep).encode(enc) expectedLinesNoHanging = expectedLines[:] expectedLinesNoHanging[-1] += '\n' assert p.bytes() == expectedBytes @@ -723,14 +710,11 @@ def test_chunks(self, tmpdir): size = 5 p.write_text(txt) for i, chunk in enumerate(p.chunks(size)): - assert chunk == txt[i * size:i * size + size] + assert chunk == txt[i * size : i * size + size] assert i == len(txt) / size - 1 - @pytest.mark.skipif( - not hasattr(os.path, 'samefile'), - reason="samefile not present", - ) + @pytest.mark.skipif(not hasattr(os.path, 'samefile'), reason="samefile not present") def test_samefile(self, tmpdir): f1 = (TempDir() / '1.txt').touch() f1.write_text('foo') @@ -738,7 +722,7 @@ def test_samefile(self, tmpdir): f1.write_text('foo') f3 = (TempDir() / '3.txt').touch() f1.write_text('bar') - f4 = (TempDir() / '4.txt') + f4 = TempDir() / '4.txt' f1.copyfile(f4) assert os.path.samefile(f1, f2) == f1.samefile(f2) @@ -756,8 +740,10 @@ def test_rmtree_p(self, tmpdir): try: sub.rmtree_p() except OSError: - self.fail("Calling `rmtree_p` on non-existent directory " - "should not raise an exception.") + self.fail( + "Calling `rmtree_p` on non-existent directory " + "should not raise an exception." + ) def test_rmdir_p_exists(self, tmpdir): """ @@ -808,20 +794,18 @@ def check_link(self): def test_with_nonexisting_dst_kwargs(self): self.subdir_a.merge_tree(self.subdir_b, symlinks=True) assert self.subdir_b.isdir() - expected = set(( - self.subdir_b / self.test_file.name, - self.subdir_b / self.test_link.name, - )) + expected = set( + (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) + ) assert set(self.subdir_b.listdir()) == expected self.check_link() def test_with_nonexisting_dst_args(self): self.subdir_a.merge_tree(self.subdir_b, True) assert self.subdir_b.isdir() - expected = set(( - self.subdir_b / self.test_file.name, - self.subdir_b / self.test_link.name, - )) + expected = set( + (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) + ) assert set(self.subdir_b.listdir()) == expected self.check_link() @@ -838,11 +822,13 @@ def test_with_existing_dst(self): self.subdir_a.merge_tree(self.subdir_b, True) assert self.subdir_b.isdir() - expected = set(( - self.subdir_b / self.test_file.name, - self.subdir_b / self.test_link.name, - self.subdir_b / test_new.name, - )) + expected = set( + ( + self.subdir_b / self.test_file.name, + self.subdir_b / self.test_link.name, + self.subdir_b / test_new.name, + ) + ) assert set(self.subdir_b.listdir()) == expected self.check_link() assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 @@ -866,8 +852,7 @@ def test_only_newer(self): target = self.subdir_b / 'testfile.txt' target.write_text('this is newer') self.subdir_a.merge_tree( - self.subdir_b, - copy_function=path.only_newer(shutil.copy2), + self.subdir_b, copy_function=path.only_newer(shutil.copy2) ) assert target.text() == 'this is newer' @@ -899,21 +884,21 @@ def test_chdir_or_cd(self, tmpdir): class TestSubclass: - def test_subclass_produces_same_class(self): """ When operations are invoked on a subclass, they should produce another instance of that subclass. """ + class PathSubclass(Path): pass + p = PathSubclass('/foo') subdir = p / 'bar' assert isinstance(subdir, PathSubclass) class TestTempDir: - def test_constructor(self): """ One should be able to readily construct a temporary directory @@ -996,6 +981,7 @@ def test_fnmatch_custom_mod(self): def test_fnmatch_custom_normcase(self): def normcase(path): return path.upper() + p = Path('FooBar') assert p.fnmatch('foobar', normcase=normcase) assert p.fnmatch('FOO[ABC]AR', normcase=normcase) @@ -1058,17 +1044,21 @@ def test_walk_case_insensitive(self, tmpdir): @pytest.mark.skipif( - sys.version_info < (2, 6), - reason="in_place requires io module in Python 2.6", + sys.version_info < (2, 6), reason="in_place requires io module in Python 2.6" ) class TestInPlace: - reference_content = textwrap.dedent(""" + reference_content = textwrap.dedent( + """ The quick brown fox jumped over the lazy dog. - """.lstrip()) - reversed_content = textwrap.dedent(""" + """.lstrip() + ) + reversed_content = textwrap.dedent( + """ .god yzal eht revo depmuj xof nworb kciuq ehT - """.lstrip()) - alternate_content = textwrap.dedent(""" + """.lstrip() + ) + alternate_content = textwrap.dedent( + """ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation @@ -1077,7 +1067,8 @@ class TestInPlace: esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - """.lstrip()) + """.lstrip() + ) @classmethod def create_reference(cls, tmpdir): @@ -1244,11 +1235,7 @@ def test_no_dependencies(): Path.py guarantees that the path module can be transplanted into an environment without any dependencies. """ - cmd = [ - sys.executable, - '-S', - '-c', 'import path', - ] + cmd = [sys.executable, '-S', '-c', 'import path'] subprocess.check_call(cmd) From 8e7c267538204284067e1fa70d53fce9f78c60f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Nov 2019 22:11:19 -0500 Subject: [PATCH 276/835] Normalize indentation --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index d267e16d..64eb21d5 100644 --- a/tox.ini +++ b/tox.ini @@ -20,11 +20,11 @@ extras = testing [testenv:build-docs] extras = - docs - testing + docs + testing changedir = docs commands = - python -m sphinx . {toxinidir}/build/html + python -m sphinx . {toxinidir}/build/html [testenv:release] skip_install = True From e73af40eb8ac08df915df3d9c419d78f23306369 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Nov 2019 22:15:05 -0500 Subject: [PATCH 277/835] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9c75febb..ff77813d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v12.0.2 +------- + +- Refreshed package metadata. + 12.0.1 ------ From 5061a873541947a8bbd756f9ee6b9c760f9b33b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Nov 2019 22:21:54 -0500 Subject: [PATCH 278/835] Fix typo in indentation (!dm filemerge) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fab5e553..fd80ef5a 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ deps = pep517>=0.5 twine>=1.13 path.py -jaraco.tidelift + jaraco.tidelift passenv = TWINE_PASSWORD TIDELIFT_TOKEN From a0651976d78d84a22a5d06807d46194218a1fefa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 7 Nov 2019 10:54:31 -0800 Subject: [PATCH 279/835] Include keyring support from twine --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 64eb21d5..d86e4ad9 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ commands = skip_install = True deps = pep517>=0.5 - twine>=1.13 + twine[keyring]>=1.13 path.py passenv = TWINE_PASSWORD From 757b121d940f0daf7fbe4f494b47cb1b0ed6e0c2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 10:31:33 -0500 Subject: [PATCH 280/835] Rename 'build-docs' to simply 'docs' (matching more popular convention). --- skeleton.md | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skeleton.md b/skeleton.md index 52b97f09..ee26e14b 100644 --- a/skeleton.md +++ b/skeleton.md @@ -85,7 +85,7 @@ The skeleton assumes the developer has [tox](https://pypi.org/project/tox) insta Other environments (invoked with `tox -e {name}`) supplied include: - - a `build-docs` environment to build the documentation + - a `docs` environment to build the documentation - a `release` environment to publish the package to PyPI A pytest.ini is included to define common options around running tests. In particular: @@ -122,7 +122,7 @@ TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD ## Building Documentation -Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e build-docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. +Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. diff --git a/tox.ini b/tox.ini index d86e4ad9..889af7a4 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = usedevelop = True extras = testing -[testenv:build-docs] +[testenv:docs] extras = docs testing From bc602bf10f189d069154dd0d37f76d845c553e01 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Fri, 29 Nov 2019 10:59:36 -0500 Subject: [PATCH 281/835] Add test capturing expectation. Ref #171. --- test_path.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test_path.py b/test_path.py index 8bd67575..9686bb20 100644 --- a/test_path.py +++ b/test_path.py @@ -765,6 +765,18 @@ def test_rmdir_p_nonexistent(self, tmpdir): assert not sub.exists() sub.rmdir_p() + def test_rmdir_p_sub_sub_dir(self, tmpdir): + """ + A non-empty folder should not raise an exception. + """ + d = Path(tmpdir) + sub = d / 'subfolder' + sub.mkdir() + subsub = sub / 'subfolder' + subsub.mkdir() + + sub.rmdir_p() + class TestMergeTree: @pytest.fixture(autouse=True) From d8490cca0b8d250972945ba5810d74354e803e18 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 11:16:23 -0500 Subject: [PATCH 282/835] Since replacing the __class__ is not allowed, create a new exception. Fixes #171. --- path/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 13a9a503..796c4a7f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1498,10 +1498,9 @@ class DirectoryNotEmpty(OSError): def translate(): try: yield - except OSError: - _, e, _ = sys.exc_info() - if e.errno == errno.ENOTEMPTY: - e.__class__ = DirectoryNotEmpty + except OSError as exc: + if exc.errno == errno.ENOTEMPTY: + raise DirectoryNotEmpty(*exc.args) from exc raise From 4167b1e93434f35ee0ddd3ab0ae2ef94bbc52c29 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 11:19:14 -0500 Subject: [PATCH 283/835] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ff77813d..b685b6e6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v12.0.3 +------- + +- #171: Fixed exception in ``rmdir_p`` when target is not empty. + v12.0.2 ------- From 39d35320b69e4646241c902577f20cb9fe28a531 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 11:23:43 -0500 Subject: [PATCH 284/835] Rely on importlib.metadata in Python 3.8. Fixes #174. --- CHANGES.rst | 1 + path/__init__.py | 7 +++++-- setup.cfg | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b685b6e6..73bf7c55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,7 @@ v12.0.3 ------- - #171: Fixed exception in ``rmdir_p`` when target is not empty. +- #174: Rely on ``importlib.metadata`` on Python 3.8. v12.0.2 ------- diff --git a/path/__init__.py b/path/__init__.py index 796c4a7f..9e0d54c4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -68,9 +68,12 @@ try: - import importlib_metadata + from importlib import metadata +except ImportError: + import importlib_metadata as metadata - __version__ = importlib_metadata.version('path.py') +try: + __version__ = metadata.version('path.py') except Exception: __version__ = 'unknown' diff --git a/setup.cfg b/setup.cfg index 84e7468b..3fe756a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = find: include_package_data = true python_requires = >=3.5 install_requires = - importlib_metadata>=0.5 + importlib_metadata >= 0.5; python_version < "3.8" setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] From 45af0e8dcadb6a09237f75cb50c8b44f83d37f4c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 11:27:35 -0500 Subject: [PATCH 285/835] Bump minor version for change in behavior (importlib.metadata). --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 73bf7c55..7676d9d7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -v12.0.3 +v12.1.0 ------- - #171: Fixed exception in ``rmdir_p`` when target is not empty. From 43444b4a070b4179b9469ddf50c6fd53932efb63 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:09:38 -0500 Subject: [PATCH 286/835] Restore support for dependency-free installation --- path/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 9e0d54c4..f9c6f707 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -70,7 +70,10 @@ try: from importlib import metadata except ImportError: - import importlib_metadata as metadata + try: + import importlib_metadata as metadata + except ImportError: + pass try: __version__ = metadata.version('path.py') From aac60dad78b0bcb1f1560ce4fff3a215e71c9ec6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 11:38:40 -0500 Subject: [PATCH 287/835] Convert from path.py to path (pie). Fixes #169. --- .github/FUNDING.yml | 2 +- README.rst | 60 ++++++++++++++++++++++----------------------- docs/index.rst | 4 +-- path/__init__.py | 7 +++--- setup.cfg | 4 +-- test_path.py | 11 +++++---- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7ab36f11..eb6c7653 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: pypi/path.py +tidelift: pypi/path diff --git a/README.rst b/README.rst index ce923b06..3e7710ed 100644 --- a/README.rst +++ b/README.rst @@ -1,28 +1,28 @@ -.. image:: https://img.shields.io/pypi/v/path.py.svg - :target: https://pypi.org/project/path.py +.. image:: https://img.shields.io/pypi/v/path.svg + :target: https://pypi.org/project/path -.. image:: https://img.shields.io/pypi/pyversions/path.py.svg +.. image:: https://img.shields.io/pypi/pyversions/path.svg -.. image:: https://img.shields.io/travis/jaraco/path.py/master.svg - :target: https://travis-ci.org/jaraco/path.py +.. image:: https://img.shields.io/travis/jaraco/path/master.svg + :target: https://travis-ci.org/jaraco/path .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black -.. image:: https://img.shields.io/appveyor/ci/jaraco/path-py/master.svg - :target: https://ci.appveyor.com/project/jaraco/path-py/branch/master +.. image:: https://img.shields.io/appveyor/ci/jaraco/path/master.svg + :target: https://ci.appveyor.com/project/jaraco/path/branch/master -.. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest - :target: https://pathpy.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/path/badge/?version=latest + :target: https://path.readthedocs.io/en/latest/?badge=latest -.. image:: https://tidelift.com/badges/package/pypi/path.py - :target: https://tidelift.com/subscription/pkg/pypi-path.py?utm_source=pypi-path.py&utm_medium=readme +.. image:: https://tidelift.com/badges/package/pypi/path + :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme -``path.py`` implements path objects as first-class entities, allowing -common operations on files to be invoked on those path objects directly. For -example: +``path`` (aka path.py or path pie) implements path objects as first-class +entities, allowing common operations on files to be invoked on those path +objects directly. For example: .. code-block:: python @@ -43,26 +43,26 @@ example: # Concatenate paths with / foo_txt = Path("bar") / "foo.txt" -``path.py`` is `hosted at Github `_. +Path pie is `hosted at Github `_. -Find `the documentation here `_. +Find `the documentation here `_. Guides and Testimonials ======================= Yasoob wrote the Python 101 `Writing a Cleanup Script `_ -based on ``path.py``. +based on ``path``. Installing ========== -Path.py may be installed using ``setuptools``, ``distribute``, or ``pip``:: +Path pie may be installed using ``setuptools``, ``distribute``, or ``pip``:: - pip install path.py + pip install path The latest release is always updated to the `Python Package Index -`_. +`_. You may also always download the source distribution (zip/tarball), extract it, and run ``python setup.py`` to install it. @@ -72,14 +72,14 @@ Advantages Python 3.4 introduced `pathlib `_, -which shares many characteristics with ``path.py``. In particular, +which shares many characteristics with ``path``. In particular, it provides an object encapsulation for representing filesystem paths. -One may have imagined ``pathlib`` would supersede ``path.py``. +One may have imagined ``pathlib`` would supersede ``path``. -But the implementation and the usage quickly diverge, and ``path.py`` +But the implementation and the usage quickly diverge, and ``path`` has several advantages over ``pathlib``: -- ``path.py`` implements ``Path`` objects as a subclass of +- ``path`` implements ``Path`` objects as a subclass of ``str``, and as a result these ``Path`` objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one @@ -87,19 +87,19 @@ has several advantages over ``pathlib``: APIs unaware of ``pathlib``. This shortcoming was `addressed by PEP 519 `_, in Python 3.6. -- ``path.py`` goes beyond exposing basic functionality of a path +- ``path`` goes beyond exposing basic functionality of a path and exposes commonly-used behaviors on a path, providing methods like ``rmtree`` (from shlib) and ``remove_p`` (remove a file if it exists). -- As a PyPI-hosted package, ``path.py`` is free to iterate +- As a PyPI-hosted package, ``path`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. -- ``path.py`` provides a uniform abstraction over its Path object, +- ``path`` provides a uniform abstraction over its Path object, freeing the implementer to subclass it readily. One cannot subclass a ``pathlib.Path`` to add functionality, but must subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even if one only wishes to add a ``__dict__`` to the subclass - instances. ``path.py`` instead allows the ``Path.module`` + instances. ``path`` instead allows the ``Path.module`` object to be overridden by subclasses, defaulting to the ``os.path``. Even advanced uses of ``path.Path`` that subclass the model do not need to be concerned with @@ -112,7 +112,7 @@ In addition to `pathlib `_, the `pylib project `_ implements a `LocalPath `_ -class, which shares some behaviors and interfaces with ``path.py``. +class, which shares some behaviors and interfaces with ``path``. Development =========== @@ -122,7 +122,7 @@ download a snapshot of the latest code. Alternatively, if you have git installed, you may be able to use ``pip`` to install directly from the repository:: - pip install git+https://github.com/jaraco/path.py.git + pip install git+https://github.com/jaraco/path.git Testing ======= diff --git a/docs/index.rst b/docs/index.rst index 1ed9774b..e8dcb148 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ -Welcome to path.py's documentation! +Welcome to Path Pie's documentation! =================================== -Contents: +Contents: .. toctree:: :maxdepth: 1 diff --git a/path/__init__.py b/path/__init__.py index f9c6f707..240973f4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1,7 +1,8 @@ """ -path.py - An object representing a path to a file or directory. +Path Pie -https://github.com/jaraco/path.py +Implements ``path.Path`` - An object representing a +path to a file or directory. Example:: @@ -76,7 +77,7 @@ pass try: - __version__ = metadata.version('path.py') + __version__ = metadata.version('path') except Exception: __version__ = 'unknown' diff --git a/setup.cfg b/setup.cfg index 3fe756a0..222f64dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,13 @@ [metadata] license_file = LICENSE -name = path.py +name = path author = Jason Orendorff author_email=jason.orendorff@gmail.com maintainer = Jason R. Coombs maintainer_email = jaraco@jaraco.com description = A module wrapper for os.path long_description = file:README.rst -url = https://github.com/jaraco/path.py +url = https://github.com/jaraco/path classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers diff --git a/test_path.py b/test_path.py index 9686bb20..5d3723eb 100644 --- a/test_path.py +++ b/test_path.py @@ -7,8 +7,9 @@ platform support, just add appropriate pathnames for your platform (os.name) in each place where the p() function is called. Then report the result. If you can't get the test to run at all on -your platform, there's probably a bug in path.py -- please report the issue -in the issue tracker at https://github.com/jaraco/path.py. +your platform, there's probably a bug in path pie -- +please report the issue +in the issue tracker at https://github.com/jaraco/path. TestScratchDir.test_touch() takes a while to run. It sleeps a few seconds to allow some time to pass between calls to check the modify @@ -241,7 +242,7 @@ def get_command_time(cmd): def test_import_time(self, monkeypatch): """ - Import of path.py should take less than some limit. + Import should take less than some limit. Run tests in a subprocess to isolate from test suite overhead. """ @@ -427,7 +428,7 @@ def test_listdir_other_encoding(self, tmpdir): # first demonstrate that os.listdir works assert os.listdir(tmpdir_bytes) - # now try with path.py + # now try with path results = Path(tmpdir).listdir() assert len(results) == 1 res, = results @@ -1244,7 +1245,7 @@ def test_iteration(self): def test_no_dependencies(): """ - Path.py guarantees that the path module can be + Path pie guarantees that the path module can be transplanted into an environment without any dependencies. """ cmd = [sys.executable, '-S', '-c', 'import path'] From b912a158bce22cc66c4384d1ca4da2deaac339aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 12:18:18 -0500 Subject: [PATCH 288/835] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7676d9d7..c6c2d6a9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v13.0.0 +------- + +- #169: Renamed package from ``path.py`` to ``path``. The docs + make reference to a pet name "path pie" for easier discovery. + v12.1.0 ------- From 4b977b0643755ecfac98c50ab697bee7b2bd70a4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:32:25 -0500 Subject: [PATCH 289/835] Rename project in GitHub to jaraco/path --- README.rst | 4 ++-- path/__init__.py | 2 -- setup.cfg | 2 +- test_path.py | 10 +++++----- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index ce923b06..4d5ffb84 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ example: # Concatenate paths with / foo_txt = Path("bar") / "foo.txt" -``path.py`` is `hosted at Github `_. +``path.py`` is `hosted at Github `_. Find `the documentation here `_. @@ -122,7 +122,7 @@ download a snapshot of the latest code. Alternatively, if you have git installed, you may be able to use ``pip`` to install directly from the repository:: - pip install git+https://github.com/jaraco/path.py.git + pip install git+https://github.com/jaraco/path.git Testing ======= diff --git a/path/__init__.py b/path/__init__.py index f9c6f707..ed237eae 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1,8 +1,6 @@ """ path.py - An object representing a path to a file or directory. -https://github.com/jaraco/path.py - Example:: from path import Path diff --git a/setup.cfg b/setup.cfg index 3fe756a0..9ebd4a39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ maintainer = Jason R. Coombs maintainer_email = jaraco@jaraco.com description = A module wrapper for os.path long_description = file:README.rst -url = https://github.com/jaraco/path.py +url = https://github.com/jaraco/path classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers diff --git a/test_path.py b/test_path.py index 9686bb20..4d910db8 100644 --- a/test_path.py +++ b/test_path.py @@ -3,14 +3,14 @@ """ Tests for the path module. -This suite runs on Linux, OS X, and Windows right now. To extend the +This suite runs on Linux, macOS, and Windows. To extend the platform support, just add appropriate pathnames for your platform (os.name) in each place where the p() function is called. -Then report the result. If you can't get the test to run at all on -your platform, there's probably a bug in path.py -- please report the issue -in the issue tracker at https://github.com/jaraco/path.py. +Then report the result. If you can't get the test to run at all on +your platform, there's probably a bug -- please report the issue +in the issue tracker. -TestScratchDir.test_touch() takes a while to run. It sleeps a few +TestScratchDir.test_touch() takes a while to run. It sleeps a few seconds to allow some time to pass between calls to check the modify time on files. """ From ba910e599fd989c5abe2a16c83f74d020cc3c52a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:36:44 -0500 Subject: [PATCH 290/835] Travis renamed also --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4d5ffb84..65cc7d91 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,8 @@ .. image:: https://img.shields.io/pypi/pyversions/path.py.svg -.. image:: https://img.shields.io/travis/jaraco/path.py/master.svg - :target: https://travis-ci.org/jaraco/path.py +.. image:: https://img.shields.io/travis/jaraco/path/master.svg + :target: https://travis-ci.org/jaraco/path .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black From 0a4605f15897571d0b8dfe1e2bf0abef8a8a9151 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:39:36 -0500 Subject: [PATCH 291/835] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7676d9d7..3f29eecd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v12.2.0 +------- + +- #169: Moved project at GitHub from ``jaraco/path.py`` to + ``jaraco/path``. + v12.1.0 ------- From 297078f0b1d885851446c94c6629d4a4f802f3a6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:40:44 -0500 Subject: [PATCH 292/835] Remove more Python 2 cruft --- path/matchers.py | 2 -- test_path.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/path/matchers.py b/path/matchers.py index 0dc2727e..053f25a8 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, unicode_literals - import ntpath import fnmatch diff --git a/test_path.py b/test_path.py index 4d910db8..5519cdb2 100644 --- a/test_path.py +++ b/test_path.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ Tests for the path module. @@ -15,8 +13,6 @@ time on files. """ -from __future__ import unicode_literals, absolute_import, print_function - import codecs import os import sys From 3935c8d256236ac5aa6d592dab49af4fbf9b7e59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 12:54:51 -0500 Subject: [PATCH 293/835] Update readme to reflect project rename. --- CHANGES.rst | 6 +++ README.rst | 149 +--------------------------------------------------- 2 files changed, 7 insertions(+), 148 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3f29eecd..bbe50ba7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v12.2.1 +------- + +- #169: Project is renamed to simply ``path``. This release is + the last planned release under the name ``path.py``. + v12.2.0 ------- diff --git a/README.rst b/README.rst index 65cc7d91..4a46c57d 100644 --- a/README.rst +++ b/README.rst @@ -1,148 +1 @@ -.. image:: https://img.shields.io/pypi/v/path.py.svg - :target: https://pypi.org/project/path.py - -.. image:: https://img.shields.io/pypi/pyversions/path.py.svg - -.. image:: https://img.shields.io/travis/jaraco/path/master.svg - :target: https://travis-ci.org/jaraco/path - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - -.. image:: https://img.shields.io/appveyor/ci/jaraco/path-py/master.svg - :target: https://ci.appveyor.com/project/jaraco/path-py/branch/master - -.. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest - :target: https://pathpy.readthedocs.io/en/latest/?badge=latest - -.. image:: https://tidelift.com/badges/package/pypi/path.py - :target: https://tidelift.com/subscription/pkg/pypi-path.py?utm_source=pypi-path.py&utm_medium=readme - - -``path.py`` implements path objects as first-class entities, allowing -common operations on files to be invoked on those path objects directly. For -example: - -.. code-block:: python - - from path import Path - d = Path('/home/guido/bin') - for f in d.files('*.py'): - f.chmod(0o755) - - # Globbing - for f in d.files('*.py'): - f.chmod(0o755) - - # Changing the working directory: - with Path("somewhere"): - # cwd in now `somewhere` - ... - - # Concatenate paths with / - foo_txt = Path("bar") / "foo.txt" - -``path.py`` is `hosted at Github `_. - -Find `the documentation here `_. - -Guides and Testimonials -======================= - -Yasoob wrote the Python 101 `Writing a Cleanup Script -`_ -based on ``path.py``. - -Installing -========== - -Path.py may be installed using ``setuptools``, ``distribute``, or ``pip``:: - - pip install path.py - -The latest release is always updated to the `Python Package Index -`_. - -You may also always download the source distribution (zip/tarball), extract -it, and run ``python setup.py`` to install it. - -Advantages -========== - -Python 3.4 introduced -`pathlib `_, -which shares many characteristics with ``path.py``. In particular, -it provides an object encapsulation for representing filesystem paths. -One may have imagined ``pathlib`` would supersede ``path.py``. - -But the implementation and the usage quickly diverge, and ``path.py`` -has several advantages over ``pathlib``: - -- ``path.py`` implements ``Path`` objects as a subclass of - ``str``, and as a result these ``Path`` - objects may be passed directly to other APIs that expect simple - text representations of paths, whereas with ``pathlib``, one - must first cast values to strings before passing them to - APIs unaware of ``pathlib``. This shortcoming was `addressed - by PEP 519 `_, - in Python 3.6. -- ``path.py`` goes beyond exposing basic functionality of a path - and exposes commonly-used behaviors on a path, providing - methods like ``rmtree`` (from shlib) and ``remove_p`` (remove - a file if it exists). -- As a PyPI-hosted package, ``path.py`` is free to iterate - faster than a stdlib package. Contributions are welcome - and encouraged. -- ``path.py`` provides a uniform abstraction over its Path object, - freeing the implementer to subclass it readily. One cannot - subclass a ``pathlib.Path`` to add functionality, but must - subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even - if one only wishes to add a ``__dict__`` to the subclass - instances. ``path.py`` instead allows the ``Path.module`` - object to be overridden by subclasses, defaulting to the - ``os.path``. Even advanced uses of ``path.Path`` that - subclass the model do not need to be concerned with - OS-specific nuances. - -Alternatives -============ - -In addition to -`pathlib `_, the -`pylib project `_ implements a -`LocalPath `_ -class, which shares some behaviors and interfaces with ``path.py``. - -Development -=========== - -To install a development version, use the Github links to clone or -download a snapshot of the latest code. Alternatively, if you have git -installed, you may be able to use ``pip`` to install directly from -the repository:: - - pip install git+https://github.com/jaraco/path.git - -Testing -======= - -Tests are invoked with `tox `_. After -having installed tox, simply invoke ``tox`` in a checkout of the repo -to invoke the tests. - -Tests are also run in continuous integration. See the badges above -for links to the CI runs. - -Releasing -========= - -Tagged releases are automatically published to PyPI by Travis-CI, assuming -the tests pass. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. +``path.py`` has been renamed to `path `. From d45b5330ee3ac4eb0411eea3e79497bd23e95245 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:52:32 -0500 Subject: [PATCH 294/835] For now, keep same docs home --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 3e7710ed..54d46ab2 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,8 @@ .. image:: https://img.shields.io/appveyor/ci/jaraco/path/master.svg :target: https://ci.appveyor.com/project/jaraco/path/branch/master -.. image:: https://readthedocs.org/projects/path/badge/?version=latest - :target: https://path.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest + :target: https://pathpy.readthedocs.io/en/latest/?badge=latest .. image:: https://tidelift.com/badges/package/pypi/path :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme @@ -45,7 +45,7 @@ objects directly. For example: Path pie is `hosted at Github `_. -Find `the documentation here `_. +Find `the documentation here `_. Guides and Testimonials ======================= From cda17ff5dcd40e83210fa7d823a117d110c08d72 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 13:54:15 -0500 Subject: [PATCH 295/835] Indicate 'path.py' as 'formerly' --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 54d46ab2..ea051faf 100644 --- a/README.rst +++ b/README.rst @@ -20,9 +20,9 @@ :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme -``path`` (aka path.py or path pie) implements path objects as first-class -entities, allowing common operations on files to be invoked on those path -objects directly. For example: +``path`` (aka path pie, formerly ``path.py``) implements path +objects as first-class entities, allowing common operations on +files to be invoked on those path objects directly. For example: .. code-block:: python From 934a382e890cbf3f7c31623cd13528eb540207cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 14:12:20 -0500 Subject: [PATCH 296/835] Remove installation instructions. Expect users to use pip as indicated in the PyPI pages. --- README.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/README.rst b/README.rst index ea051faf..a893a450 100644 --- a/README.rst +++ b/README.rst @@ -54,19 +54,6 @@ Yasoob wrote the Python 101 `Writing a Cleanup Script `_ based on ``path``. -Installing -========== - -Path pie may be installed using ``setuptools``, ``distribute``, or ``pip``:: - - pip install path - -The latest release is always updated to the `Python Package Index -`_. - -You may also always download the source distribution (zip/tarball), extract -it, and run ``python setup.py`` to install it. - Advantages ========== From 16d0c3bada2842aa68795066a1ff5cd39f326eb4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 14:15:39 -0500 Subject: [PATCH 297/835] Highlight permission mask functionality in readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a893a450..dc8512eb 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ files to be invoked on those path objects directly. For example: # Globbing for f in d.files('*.py'): - f.chmod(0o755) + f.chmod('u+rwx') # Changing the working directory: with Path("somewhere"): From 333ba404c90a6d9bb87dec70e4513982ac1e6b2e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 17:34:14 -0500 Subject: [PATCH 298/835] Move documentation to 'path.readthedocs.io' --- README.rst | 6 +++--- docs/index.rst | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index dc8512eb..72a05212 100644 --- a/README.rst +++ b/README.rst @@ -13,8 +13,8 @@ .. image:: https://img.shields.io/appveyor/ci/jaraco/path/master.svg :target: https://ci.appveyor.com/project/jaraco/path/branch/master -.. image:: https://readthedocs.org/projects/pathpy/badge/?version=latest - :target: https://pathpy.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/path/badge/?version=latest + :target: https://path.readthedocs.io/en/latest/?badge=latest .. image:: https://tidelift.com/badges/package/pypi/path :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme @@ -45,7 +45,7 @@ files to be invoked on those path objects directly. For example: Path pie is `hosted at Github `_. -Find `the documentation here `_. +Find `the documentation here `_. Guides and Testimonials ======================= diff --git a/docs/index.rst b/docs/index.rst index e8dcb148..cb47ed17 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ Welcome to Path Pie's documentation! -=================================== +==================================== Contents: @@ -9,6 +9,10 @@ Contents: api history +Thanks to Mahan Marwat for transferring the ``path`` name on +Read The Docs from `path `_ +to this project. + Indices and tables ================== From a0fba60e0157a9d640b9573d47dcf58775315899 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 17:55:13 -0500 Subject: [PATCH 299/835] Warn that package is deprecated and direct users to require path instead. --- path/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index ed237eae..6cbf2ee7 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -79,6 +79,13 @@ __version__ = 'unknown' +warnings.warn( + "path.py is now path (https://pypi.org/project/path). " + "Please switch to that package.", + DeprecationWarning, +) + + class TreeWalkWarning(Warning): pass From 9f3937b9a5485b9bf123e97df618c00ba55ace21 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 18:13:59 -0500 Subject: [PATCH 300/835] Remove everything meaningful about this package and defer to the new 'path' --- path/__init__.py | 1731 ---------------------------------------------- path/matchers.py | 59 -- setup.cfg | 2 +- test_path.py | 1255 --------------------------------- 4 files changed, 1 insertion(+), 3046 deletions(-) delete mode 100644 path/__init__.py delete mode 100644 path/matchers.py delete mode 100644 test_path.py diff --git a/path/__init__.py b/path/__init__.py deleted file mode 100644 index 6cbf2ee7..00000000 --- a/path/__init__.py +++ /dev/null @@ -1,1731 +0,0 @@ -""" -path.py - An object representing a path to a file or directory. - -Example:: - - from path import Path - d = Path('/home/guido/bin') - - # Globbing - for f in d.files('*.py'): - f.chmod(0o755) - - # Changing the working directory: - with Path("somewhere"): - # cwd in now `somewhere` - ... - - # Concatenate paths with / - foo_txt = Path("bar") / "foo.txt" -""" - -import sys -import warnings -import os -import fnmatch -import glob -import shutil -import hashlib -import errno -import tempfile -import functools -import operator -import re -import contextlib -import io -import importlib -import itertools - -try: - import win32security -except ImportError: - pass - -try: - import pwd -except ImportError: - pass - -try: - import grp -except ImportError: - pass - -from . import matchers - - -__all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] - - -LINESEPS = ['\r\n', '\r', '\n'] -U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] -NEWLINE = re.compile('|'.join(LINESEPS)) -U_NEWLINE = re.compile('|'.join(U_LINESEPS)) -NL_END = re.compile(r'(?:{0})$'.format(NEWLINE.pattern)) -U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern)) - - -try: - from importlib import metadata -except ImportError: - try: - import importlib_metadata as metadata - except ImportError: - pass - -try: - __version__ = metadata.version('path.py') -except Exception: - __version__ = 'unknown' - - -warnings.warn( - "path.py is now path (https://pypi.org/project/path). " - "Please switch to that package.", - DeprecationWarning, -) - - -class TreeWalkWarning(Warning): - pass - - -# from jaraco.functools -def compose(*funcs): - compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa - return functools.reduce(compose_two, funcs) - - -def simple_cache(func): - """ - Save results for the :meth:'path.using_module' classmethod. - When Python 3.2 is available, use functools.lru_cache instead. - """ - saved_results = {} - - def wrapper(cls, module): - if module in saved_results: - return saved_results[module] - saved_results[module] = func(cls, module) - return saved_results[module] - - return wrapper - - -class ClassProperty(property): - def __get__(self, cls, owner): - return self.fget.__get__(None, owner)() - - -class multimethod(object): - """ - Acts like a classmethod when invoked from the class and like an - instancemethod when invoked from the instance. - """ - - def __init__(self, func): - self.func = func - - def __get__(self, instance, owner): - return ( - functools.partial(self.func, owner) - if instance is None - else functools.partial(self.func, owner, instance) - ) - - -class Path(str): - """ - Represents a filesystem path. - - For documentation on individual methods, consult their - counterparts in :mod:`os.path`. - - Some methods are additionally included from :mod:`shutil`. - The functions are linked directly into the class namespace - such that they will be bound to the Path instance. For example, - ``Path(src).copy(target)`` is equivalent to - ``shutil.copy(src, target)``. Therefore, when referencing - the docs for these methods, assume `src` references `self`, - the Path instance. - """ - - module = os.path - """ The path module to use for path operations. - - .. seealso:: :mod:`os.path` - """ - - def __init__(self, other=''): - if other is None: - raise TypeError("Invalid initial value for path: None") - - @classmethod - @simple_cache - def using_module(cls, module): - subclass_name = cls.__name__ + '_' + module.__name__ - bases = (cls,) - ns = {'module': module} - return type(subclass_name, bases, ns) - - @ClassProperty - @classmethod - def _next_class(cls): - """ - What class should be used to construct new instances from this class - """ - return cls - - # --- Special Python methods. - - def __repr__(self): - return '%s(%s)' % (type(self).__name__, super(Path, self).__repr__()) - - # Adding a Path and a string yields a Path. - def __add__(self, more): - try: - return self._next_class(super(Path, self).__add__(more)) - except TypeError: # Python bug - return NotImplemented - - def __radd__(self, other): - if not isinstance(other, str): - return NotImplemented - return self._next_class(other.__add__(self)) - - # The / operator joins Paths. - def __div__(self, rel): - """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) - - Join two path components, adding a separator character if - needed. - - .. seealso:: :func:`os.path.join` - """ - return self._next_class(self.module.join(self, rel)) - - # Make the / operator work even when true division is enabled. - __truediv__ = __div__ - - # The / operator joins Paths the other way around - def __rdiv__(self, rel): - """ fp.__rdiv__(rel) == rel / fp - - Join two path components, adding a separator character if - needed. - - .. seealso:: :func:`os.path.join` - """ - return self._next_class(self.module.join(rel, self)) - - # Make the / operator work even when true division is enabled. - __rtruediv__ = __rdiv__ - - def __enter__(self): - self._old_dir = self.getcwd() - os.chdir(self) - return self - - def __exit__(self, *_): - os.chdir(self._old_dir) - - def __fspath__(self): - return self - - @classmethod - def getcwd(cls): - """ Return the current working directory as a path object. - - .. seealso:: :func:`os.getcwdu` - """ - return cls(os.getcwd()) - - # - # --- Operations on Path strings. - - def abspath(self): - """ .. seealso:: :func:`os.path.abspath` """ - return self._next_class(self.module.abspath(self)) - - def normcase(self): - """ .. seealso:: :func:`os.path.normcase` """ - return self._next_class(self.module.normcase(self)) - - def normpath(self): - """ .. seealso:: :func:`os.path.normpath` """ - return self._next_class(self.module.normpath(self)) - - def realpath(self): - """ .. seealso:: :func:`os.path.realpath` """ - return self._next_class(self.module.realpath(self)) - - def expanduser(self): - """ .. seealso:: :func:`os.path.expanduser` """ - return self._next_class(self.module.expanduser(self)) - - def expandvars(self): - """ .. seealso:: :func:`os.path.expandvars` """ - return self._next_class(self.module.expandvars(self)) - - def dirname(self): - """ .. seealso:: :attr:`parent`, :func:`os.path.dirname` """ - return self._next_class(self.module.dirname(self)) - - def basename(self): - """ .. seealso:: :attr:`name`, :func:`os.path.basename` """ - return self._next_class(self.module.basename(self)) - - def expand(self): - """ Clean up a filename by calling :meth:`expandvars()`, - :meth:`expanduser()`, and :meth:`normpath()` on it. - - This is commonly everything needed to clean up a filename - read from a configuration file, for example. - """ - return self.expandvars().expanduser().normpath() - - @property - def stem(self): - """ The same as :meth:`name`, but with one file extension stripped off. - - >>> Path('/home/guido/python.tar.gz').stem - 'python.tar' - """ - base, ext = self.module.splitext(self.name) - return base - - @property - def namebase(self): - warnings.warn("Use .stem instead of .namebase", DeprecationWarning) - return self.stem - - @property - def ext(self): - """ The file extension, for example ``'.py'``. """ - f, ext = self.module.splitext(self) - return ext - - def with_suffix(self, suffix): - """ Return a new path with the file suffix changed (or added, if none) - - >>> Path('/home/guido/python.tar.gz').with_suffix(".foo") - Path('/home/guido/python.tar.foo') - - >>> Path('python').with_suffix('.zip') - Path('python.zip') - - >>> Path('filename.ext').with_suffix('zip') - Traceback (most recent call last): - ... - ValueError: Invalid suffix 'zip' - """ - if not suffix.startswith('.'): - raise ValueError("Invalid suffix {suffix!r}".format(**locals())) - - return self.stripext() + suffix - - @property - def drive(self): - """ The drive specifier, for example ``'C:'``. - - This is always empty on systems that don't use drive specifiers. - """ - drive, r = self.module.splitdrive(self) - return self._next_class(drive) - - parent = property( - dirname, - None, - None, - """ This path's parent directory, as a new Path object. - - For example, - ``Path('/usr/local/lib/libpython.so').parent == - Path('/usr/local/lib')`` - - .. seealso:: :meth:`dirname`, :func:`os.path.dirname` - """, - ) - - name = property( - basename, - None, - None, - """ The name of this file or directory without the full path. - - For example, - ``Path('/usr/local/lib/libpython.so').name == 'libpython.so'`` - - .. seealso:: :meth:`basename`, :func:`os.path.basename` - """, - ) - - def splitpath(self): - """ p.splitpath() -> Return ``(p.parent, p.name)``. - - .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split` - """ - parent, child = self.module.split(self) - return self._next_class(parent), child - - def splitdrive(self): - """ p.splitdrive() -> Return ``(p.drive, )``. - - Split the drive specifier from this path. If there is - no drive specifier, :samp:`{p.drive}` is empty, so the return value - is simply ``(Path(''), p)``. This is always the case on Unix. - - .. seealso:: :func:`os.path.splitdrive` - """ - drive, rel = self.module.splitdrive(self) - return self._next_class(drive), rel - - def splitext(self): - """ p.splitext() -> Return ``(p.stripext(), p.ext)``. - - Split the filename extension from this path and return - the two parts. Either part may be empty. - - The extension is everything from ``'.'`` to the end of the - last path segment. This has the property that if - ``(a, b) == p.splitext()``, then ``a + b == p``. - - .. seealso:: :func:`os.path.splitext` - """ - filename, ext = self.module.splitext(self) - return self._next_class(filename), ext - - def stripext(self): - """ p.stripext() -> Remove one file extension from the path. - - For example, ``Path('/home/guido/python.tar.gz').stripext()`` - returns ``Path('/home/guido/python.tar')``. - """ - return self.splitext()[0] - - def splitunc(self): - """ .. seealso:: :func:`os.path.splitunc` """ - unc, rest = self.module.splitunc(self) - return self._next_class(unc), rest - - @property - def uncshare(self): - """ - The UNC mount point for this path. - This is empty for paths on local drives. - """ - unc, r = self.module.splitunc(self) - return self._next_class(unc) - - @multimethod - def joinpath(cls, first, *others): - """ - Join first to zero or more :class:`Path` components, - adding a separator character (:samp:`{first}.module.sep`) - if needed. Returns a new instance of - :samp:`{first}._next_class`. - - .. seealso:: :func:`os.path.join` - """ - if not isinstance(first, cls): - first = cls(first) - return first._next_class(first.module.join(first, *others)) - - def splitall(self): - r""" Return a list of the path components in this path. - - The first item in the list will be a Path. Its value will be - either :data:`os.curdir`, :data:`os.pardir`, empty, or the root - directory of this path (for example, ``'/'`` or ``'C:\\'``). The - other items in the list will be strings. - - ``path.Path.joinpath(*result)`` will yield the original path. - """ - parts = [] - loc = self - while loc != os.curdir and loc != os.pardir: - prev = loc - loc, child = prev.splitpath() - if loc == prev: - break - parts.append(child) - parts.append(loc) - parts.reverse() - return parts - - def relpath(self, start='.'): - """ Return this path as a relative path, - based from `start`, which defaults to the current working directory. - """ - cwd = self._next_class(start) - return cwd.relpathto(self) - - def relpathto(self, dest): - """ Return a relative path from `self` to `dest`. - - If there is no relative path from `self` to `dest`, for example if - they reside on different drives in Windows, then this returns - ``dest.abspath()``. - """ - origin = self.abspath() - dest = self._next_class(dest).abspath() - - orig_list = origin.normcase().splitall() - # Don't normcase dest! We want to preserve the case. - dest_list = dest.splitall() - - if orig_list[0] != self.module.normcase(dest_list[0]): - # Can't get here from there. - return dest - - # Find the location where the two paths start to differ. - i = 0 - for start_seg, dest_seg in zip(orig_list, dest_list): - if start_seg != self.module.normcase(dest_seg): - break - i += 1 - - # Now i is the point where the two paths diverge. - # Need a certain number of "os.pardir"s to work up - # from the origin to the point of divergence. - segments = [os.pardir] * (len(orig_list) - i) - # Need to add the diverging part of dest_list. - segments += dest_list[i:] - if len(segments) == 0: - # If they happen to be identical, use os.curdir. - relpath = os.curdir - else: - relpath = self.module.join(*segments) - return self._next_class(relpath) - - # --- Listing, searching, walking, and matching - - def listdir(self, match=None): - """ D.listdir() -> List of items in this directory. - - Use :meth:`files` or :meth:`dirs` instead if you want a listing - of just files or just subdirectories. - - The elements of the list are Path objects. - - With the optional `match` argument, a callable, - only return items whose names match the given pattern. - - .. seealso:: :meth:`files`, :meth:`dirs` - """ - match = matchers.load(match) - return list(filter(match, (self / child for child in os.listdir(self)))) - - def dirs(self, *args, **kwargs): - """ D.dirs() -> List of this directory's subdirectories. - - The elements of the list are Path objects. - This does not walk recursively into subdirectories - (but see :meth:`walkdirs`). - - Accepts parameters to :meth:`listdir`. - """ - return [p for p in self.listdir(*args, **kwargs) if p.isdir()] - - def files(self, *args, **kwargs): - """ D.files() -> List of the files in this directory. - - The elements of the list are Path objects. - This does not walk into subdirectories (see :meth:`walkfiles`). - - Accepts parameters to :meth:`listdir`. - """ - - return [p for p in self.listdir(*args, **kwargs) if p.isfile()] - - def walk(self, match=None, errors='strict'): - """ D.walk() -> iterator over files and subdirs, recursively. - - The iterator yields Path objects naming each child item of - this directory and its descendants. This requires that - ``D.isdir()``. - - This performs a depth-first traversal of the directory tree. - Each directory is returned just before all its children. - - The `errors=` keyword argument controls behavior when an - error occurs. The default is ``'strict'``, which causes an - exception. Other allowed values are ``'warn'`` (which - reports the error via :func:`warnings.warn()`), and ``'ignore'``. - `errors` may also be an arbitrary callable taking a msg parameter. - """ - - class Handlers: - def strict(msg): - raise - - def warn(msg): - warnings.warn(msg, TreeWalkWarning) - - def ignore(msg): - pass - - if not callable(errors) and errors not in vars(Handlers): - raise ValueError("invalid errors parameter") - errors = vars(Handlers).get(errors, errors) - - match = matchers.load(match) - - try: - childList = self.listdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to list directory '%(self)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - return - - for child in childList: - if match(child): - yield child - try: - isdir = child.isdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to access '%(child)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - isdir = False - - if isdir: - for item in child.walk(errors=errors, match=match): - yield item - - def walkdirs(self, *args, **kwargs): - """ D.walkdirs() -> iterator over subdirs, recursively. - """ - return (item for item in self.walk(*args, **kwargs) if item.isdir()) - - def walkfiles(self, *args, **kwargs): - """ D.walkfiles() -> iterator over files in D, recursively. - """ - return (item for item in self.walk(*args, **kwargs) if item.isfile()) - - def fnmatch(self, pattern, normcase=None): - """ Return ``True`` if `self.name` matches the given `pattern`. - - `pattern` - A filename pattern with wildcards, - for example ``'*.py'``. If the pattern contains a `normcase` - attribute, it is applied to the name and path prior to comparison. - - `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, which - defaults to :meth:`os.path.normcase`. - - .. seealso:: :func:`fnmatch.fnmatch` - """ - default_normcase = getattr(pattern, 'normcase', self.module.normcase) - normcase = normcase or default_normcase - name = normcase(self.name) - pattern = normcase(pattern) - return fnmatch.fnmatchcase(name, pattern) - - def glob(self, pattern): - """ Return a list of Path objects that match the pattern. - - `pattern` - a path relative to this directory, with wildcards. - - For example, ``Path('/users').glob('*/bin/*')`` returns a list - of all the files users have in their :file:`bin` directories. - - .. seealso:: :func:`glob.glob` - - .. note:: Glob is **not** recursive, even when using ``**``. - To do recursive globbing see :func:`walk`, - :func:`walkdirs` or :func:`walkfiles`. - """ - cls = self._next_class - return [cls(s) for s in glob.glob(self / pattern)] - - def iglob(self, pattern): - """ Return an iterator of Path objects that match the pattern. - - `pattern` - a path relative to this directory, with wildcards. - - For example, ``Path('/users').iglob('*/bin/*')`` returns an - iterator of all the files users have in their :file:`bin` - directories. - - .. seealso:: :func:`glob.iglob` - - .. note:: Glob is **not** recursive, even when using ``**``. - To do recursive globbing see :func:`walk`, - :func:`walkdirs` or :func:`walkfiles`. - """ - cls = self._next_class - return (cls(s) for s in glob.iglob(self / pattern)) - - # - # --- Reading or writing an entire file at once. - - def open(self, *args, **kwargs): - """ Open this file and return a corresponding :class:`file` object. - - Keyword arguments work as in :func:`io.open`. If the file cannot be - opened, an :class:`~exceptions.OSError` is raised. - """ - return io.open(self, *args, **kwargs) - - def bytes(self): - """ Open this file, read all bytes, return them as a string. """ - with self.open('rb') as f: - return f.read() - - def chunks(self, size, *args, **kwargs): - """ Returns a generator yielding chunks of the file, so it can - be read piece by piece with a simple for loop. - - Any argument you pass after `size` will be passed to :meth:`open`. - - :example: - - >>> hash = hashlib.md5() - >>> for chunk in Path("CHANGES.rst").chunks(8192, mode='rb'): - ... hash.update(chunk) - - This will read the file by chunks of 8192 bytes. - """ - with self.open(*args, **kwargs) as f: - for chunk in iter(lambda: f.read(size) or None, None): - yield chunk - - def write_bytes(self, bytes, append=False): - """ Open this file and write the given bytes to it. - - Default behavior is to overwrite any existing file. - Call ``p.write_bytes(bytes, append=True)`` to append instead. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - with self.open(mode) as f: - f.write(bytes) - - def text(self, encoding=None, errors='strict'): - r""" Open this file, read it in, return the content as a string. - - All newline sequences are converted to ``'\n'``. Keyword arguments - will be passed to :meth:`open`. - - .. seealso:: :meth:`lines` - """ - with self.open(mode='r', encoding=encoding, errors=errors) as f: - return U_NEWLINE.sub('\n', f.read()) - - def write_text( - self, text, encoding=None, errors='strict', linesep=os.linesep, append=False - ): - r""" Write the given text to this file. - - The default behavior is to overwrite any existing file; - to append instead, use the `append=True` keyword argument. - - There are two differences between :meth:`write_text` and - :meth:`write_bytes`: newline handling and Unicode handling. - See below. - - Parameters: - - `text` - str/unicode - The text to be written. - - `encoding` - str - The Unicode encoding that will be used. - This is ignored if `text` isn't a Unicode string. - - `errors` - str - How to handle Unicode encoding errors. - Default is ``'strict'``. See ``help(unicode.encode)`` for the - options. This is ignored if `text` isn't a Unicode - string. - - `linesep` - keyword argument - str/unicode - The sequence of - characters to be used to mark end-of-line. The default is - :data:`os.linesep`. You can also specify ``None`` to - leave all newlines as they are in `text`. - - `append` - keyword argument - bool - Specifies what to do if - the file already exists (``True``: append to the end of it; - ``False``: overwrite it.) The default is ``False``. - - - --- Newline handling. - - ``write_text()`` converts all standard end-of-line sequences - (``'\n'``, ``'\r'``, and ``'\r\n'``) to your platform's default - end-of-line sequence (see :data:`os.linesep`; on Windows, for example, - the end-of-line marker is ``'\r\n'``). - - If you don't like your platform's default, you can override it - using the `linesep=` keyword argument. If you specifically want - ``write_text()`` to preserve the newlines as-is, use ``linesep=None``. - - This applies to Unicode text the same as to 8-bit text, except - there are three additional standard Unicode end-of-line sequences: - ``u'\x85'``, ``u'\r\x85'``, and ``u'\u2028'``. - - (This is slightly different from when you open a file for - writing with ``fopen(filename, "w")`` in C or ``open(filename, 'w')`` - in Python.) - - - --- Unicode - - If `text` isn't Unicode, then apart from newline handling, the - bytes are written verbatim to the file. The `encoding` and - `errors` arguments are not used and must be omitted. - - If `text` is Unicode, it is first converted to :func:`bytes` using the - specified `encoding` (or the default encoding if `encoding` - isn't specified). The `errors` argument applies only to this - conversion. - - """ - if isinstance(text, str): - if linesep is not None: - text = U_NEWLINE.sub(linesep, text) - text = text.encode(encoding or sys.getdefaultencoding(), errors) - else: - assert encoding is None - text = NEWLINE.sub(linesep, text) - self.write_bytes(text, append=append) - - def lines(self, encoding=None, errors='strict', retain=True): - r""" Open this file, read all lines, return them in a list. - - Optional arguments: - `encoding` - The Unicode encoding (or character set) of - the file. The default is ``None``, meaning the content - of the file is read as 8-bit characters and returned - as a list of (non-Unicode) str objects. - `errors` - How to handle Unicode errors; see help(str.decode) - for the options. Default is ``'strict'``. - `retain` - If ``True``, retain newline characters; but all newline - character combinations (``'\r'``, ``'\n'``, ``'\r\n'``) are - translated to ``'\n'``. If ``False``, newline characters are - stripped off. Default is ``True``. - - .. seealso:: :meth:`text` - """ - return self.text(encoding, errors).splitlines(retain) - - def write_lines( - self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False - ): - r""" Write the given lines of text to this file. - - By default this overwrites any existing file at this path. - - This puts a platform-specific newline sequence on every line. - See `linesep` below. - - `lines` - A list of strings. - - `encoding` - A Unicode encoding to use. This applies only if - `lines` contains any Unicode strings. - - `errors` - How to handle errors in Unicode encoding. This - also applies only to Unicode strings. - - linesep - The desired line-ending. This line-ending is - applied to every line. If a line already has any - standard line ending (``'\r'``, ``'\n'``, ``'\r\n'``, - ``u'\x85'``, ``u'\r\x85'``, ``u'\u2028'``), that will - be stripped off and this will be used instead. The - default is os.linesep, which is platform-dependent - (``'\r\n'`` on Windows, ``'\n'`` on Unix, etc.). - Specify ``None`` to write the lines as-is, like - :meth:`file.writelines`. - - Use the keyword argument ``append=True`` to append lines to the - file. The default is to overwrite the file. - - .. warning :: - - When you use this with Unicode data, if the encoding of the - existing data in the file is different from the encoding - you specify with the `encoding=` parameter, the result is - mixed-encoding data, which can really confuse someone trying - to read the file later. - """ - with self.open('ab' if append else 'wb') as f: - for line in lines: - isUnicode = isinstance(line, str) - if linesep is not None: - pattern = U_NL_END if isUnicode else NL_END - line = pattern.sub('', line) + linesep - if isUnicode: - line = line.encode(encoding or sys.getdefaultencoding(), errors) - f.write(line) - - def read_md5(self): - """ Calculate the md5 hash for this file. - - This reads through the entire file. - - .. seealso:: :meth:`read_hash` - """ - return self.read_hash('md5') - - def _hash(self, hash_name): - """ Returns a hash object for the file at the current path. - - `hash_name` should be a hash algo name (such as ``'md5'`` - or ``'sha1'``) that's available in the :mod:`hashlib` module. - """ - m = hashlib.new(hash_name) - for chunk in self.chunks(8192, mode="rb"): - m.update(chunk) - return m - - def read_hash(self, hash_name): - """ Calculate given hash for this file. - - List of supported hashes can be obtained from :mod:`hashlib` package. - This reads the entire file. - - .. seealso:: :meth:`hashlib.hash.digest` - """ - return self._hash(hash_name).digest() - - def read_hexhash(self, hash_name): - """ Calculate given hash for this file, returning hexdigest. - - List of supported hashes can be obtained from :mod:`hashlib` package. - This reads the entire file. - - .. seealso:: :meth:`hashlib.hash.hexdigest` - """ - return self._hash(hash_name).hexdigest() - - # --- Methods for querying the filesystem. - # N.B. On some platforms, the os.path functions may be implemented in C - # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get - # bound. Playing it safe and wrapping them all in method calls. - - def isabs(self): - """ .. seealso:: :func:`os.path.isabs` """ - return self.module.isabs(self) - - def exists(self): - """ .. seealso:: :func:`os.path.exists` """ - return self.module.exists(self) - - def isdir(self): - """ .. seealso:: :func:`os.path.isdir` """ - return self.module.isdir(self) - - def isfile(self): - """ .. seealso:: :func:`os.path.isfile` """ - return self.module.isfile(self) - - def islink(self): - """ .. seealso:: :func:`os.path.islink` """ - return self.module.islink(self) - - def ismount(self): - """ .. seealso:: :func:`os.path.ismount` """ - return self.module.ismount(self) - - def samefile(self, other): - """ .. seealso:: :func:`os.path.samefile` """ - if not hasattr(self.module, 'samefile'): - other = Path(other).realpath().normpath().normcase() - return self.realpath().normpath().normcase() == other - return self.module.samefile(self, other) - - def getatime(self): - """ .. seealso:: :attr:`atime`, :func:`os.path.getatime` """ - return self.module.getatime(self) - - atime = property( - getatime, - None, - None, - """ Last access time of the file. - - .. seealso:: :meth:`getatime`, :func:`os.path.getatime` - """, - ) - - def getmtime(self): - """ .. seealso:: :attr:`mtime`, :func:`os.path.getmtime` """ - return self.module.getmtime(self) - - mtime = property( - getmtime, - None, - None, - """ Last-modified time of the file. - - .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` - """, - ) - - def getctime(self): - """ .. seealso:: :attr:`ctime`, :func:`os.path.getctime` """ - return self.module.getctime(self) - - ctime = property( - getctime, - None, - None, - """ Creation time of the file. - - .. seealso:: :meth:`getctime`, :func:`os.path.getctime` - """, - ) - - def getsize(self): - """ .. seealso:: :attr:`size`, :func:`os.path.getsize` """ - return self.module.getsize(self) - - size = property( - getsize, - None, - None, - """ Size of the file, in bytes. - - .. seealso:: :meth:`getsize`, :func:`os.path.getsize` - """, - ) - - if hasattr(os, 'access'): - - def access(self, mode): - """ Return ``True`` if current user has access to this path. - - mode - One of the constants :data:`os.F_OK`, :data:`os.R_OK`, - :data:`os.W_OK`, :data:`os.X_OK` - - .. seealso:: :func:`os.access` - """ - return os.access(self, mode) - - def stat(self): - """ Perform a ``stat()`` system call on this path. - - .. seealso:: :meth:`lstat`, :func:`os.stat` - """ - return os.stat(self) - - def lstat(self): - """ Like :meth:`stat`, but do not follow symbolic links. - - .. seealso:: :meth:`stat`, :func:`os.lstat` - """ - return os.lstat(self) - - def __get_owner_windows(self): - """ - Return the name of the owner of this file or directory. Follow - symbolic links. - - Return a name of the form ``r'DOMAIN\\User Name'``; may be a group. - - .. seealso:: :attr:`owner` - """ - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION - ) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + '\\' + account - - def __get_owner_unix(self): - """ - Return the name of the owner of this file or directory. Follow - symbolic links. - - .. seealso:: :attr:`owner` - """ - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name - - def __get_owner_not_implemented(self): - raise NotImplementedError("Ownership not available on this platform.") - - if 'win32security' in globals(): - get_owner = __get_owner_windows - elif 'pwd' in globals(): - get_owner = __get_owner_unix - else: - get_owner = __get_owner_not_implemented - - owner = property( - get_owner, - None, - None, - """ Name of the owner of this file or directory. - - .. seealso:: :meth:`get_owner`""", - ) - - if hasattr(os, 'statvfs'): - - def statvfs(self): - """ Perform a ``statvfs()`` system call on this path. - - .. seealso:: :func:`os.statvfs` - """ - return os.statvfs(self) - - if hasattr(os, 'pathconf'): - - def pathconf(self, name): - """ .. seealso:: :func:`os.pathconf` """ - return os.pathconf(self, name) - - # - # --- Modifying operations on files and directories - - def utime(self, times): - """ Set the access and modified times of this file. - - .. seealso:: :func:`os.utime` - """ - os.utime(self, times) - return self - - def chmod(self, mode): - """ - Set the mode. May be the new mode (os.chmod behavior) or a `symbolic - mode `_. - - .. seealso:: :func:`os.chmod` - """ - if isinstance(mode, str): - mask = _multi_permission_mask(mode) - mode = mask(self.stat().st_mode) - os.chmod(self, mode) - return self - - def chown(self, uid=-1, gid=-1): - """ - Change the owner and group by names rather than the uid or gid numbers. - - .. seealso:: :func:`os.chown` - """ - if hasattr(os, 'chown'): - if 'pwd' in globals() and isinstance(uid, str): - uid = pwd.getpwnam(uid).pw_uid - if 'grp' in globals() and isinstance(gid, str): - gid = grp.getgrnam(gid).gr_gid - os.chown(self, uid, gid) - else: - msg = "Ownership not available on this platform." - raise NotImplementedError(msg) - return self - - def rename(self, new): - """ .. seealso:: :func:`os.rename` """ - os.rename(self, new) - return self._next_class(new) - - def renames(self, new): - """ .. seealso:: :func:`os.renames` """ - os.renames(self, new) - return self._next_class(new) - - # - # --- Create/delete operations on directories - - def mkdir(self, mode=0o777): - """ .. seealso:: :func:`os.mkdir` """ - os.mkdir(self, mode) - return self - - def mkdir_p(self, mode=0o777): - """ Like :meth:`mkdir`, but does not raise an exception if the - directory already exists. """ - with contextlib.suppress(FileExistsError): - self.mkdir(mode) - return self - - def makedirs(self, mode=0o777): - """ .. seealso:: :func:`os.makedirs` """ - os.makedirs(self, mode) - return self - - def makedirs_p(self, mode=0o777): - """ Like :meth:`makedirs`, but does not raise an exception if the - directory already exists. """ - with contextlib.suppress(FileExistsError): - self.makedirs(mode) - return self - - def rmdir(self): - """ .. seealso:: :func:`os.rmdir` """ - os.rmdir(self) - return self - - def rmdir_p(self): - """ Like :meth:`rmdir`, but does not raise an exception if the - directory is not empty or does not exist. """ - suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty - with contextlib.suppress(suppressed): - with DirectoryNotEmpty.translate(): - self.rmdir() - return self - - def removedirs(self): - """ .. seealso:: :func:`os.removedirs` """ - os.removedirs(self) - return self - - def removedirs_p(self): - """ Like :meth:`removedirs`, but does not raise an exception if the - directory is not empty or does not exist. """ - with contextlib.suppress(FileExistsError, DirectoryNotEmpty): - with DirectoryNotEmpty.translate(): - self.removedirs() - return self - - # --- Modifying operations on files - - def touch(self): - """ Set the access/modified times of this file to the current time. - Create the file if it does not exist. - """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) - os.close(fd) - os.utime(self, None) - return self - - def remove(self): - """ .. seealso:: :func:`os.remove` """ - os.remove(self) - return self - - def remove_p(self): - """ Like :meth:`remove`, but does not raise an exception if the - file does not exist. """ - with contextlib.suppress(FileNotFoundError): - self.unlink() - return self - - def unlink(self): - """ .. seealso:: :func:`os.unlink` """ - os.unlink(self) - return self - - def unlink_p(self): - """ Like :meth:`unlink`, but does not raise an exception if the - file does not exist. """ - self.remove_p() - return self - - # --- Links - - if hasattr(os, 'link'): - - def link(self, newpath): - """ Create a hard link at `newpath`, pointing to this file. - - .. seealso:: :func:`os.link` - """ - os.link(self, newpath) - return self._next_class(newpath) - - if hasattr(os, 'symlink'): - - def symlink(self, newlink=None): - """ Create a symbolic link at `newlink`, pointing here. - - If newlink is not supplied, the symbolic link will assume - the name self.basename(), creating the link in the cwd. - - .. seealso:: :func:`os.symlink` - """ - if newlink is None: - newlink = self.basename() - os.symlink(self, newlink) - return self._next_class(newlink) - - if hasattr(os, 'readlink'): - - def readlink(self): - """ Return the path to which this symbolic link points. - - The result may be an absolute or a relative path. - - .. seealso:: :meth:`readlinkabs`, :func:`os.readlink` - """ - return self._next_class(os.readlink(self)) - - def readlinkabs(self): - """ Return the path to which this symbolic link points. - - The result is always an absolute path. - - .. seealso:: :meth:`readlink`, :func:`os.readlink` - """ - p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() - - # High-level functions from shutil - # These functions will be bound to the instance such that - # Path(name).copy(target) will invoke shutil.copy(name, target) - - copyfile = shutil.copyfile - copymode = shutil.copymode - copystat = shutil.copystat - copy = shutil.copy - copy2 = shutil.copy2 - copytree = shutil.copytree - if hasattr(shutil, 'move'): - move = shutil.move - rmtree = shutil.rmtree - - def rmtree_p(self): - """ Like :meth:`rmtree`, but does not raise an exception if the - directory does not exist. """ - with contextlib.suppress(FileNotFoundError): - self.rmtree() - return self - - def chdir(self): - """ .. seealso:: :func:`os.chdir` """ - os.chdir(self) - - cd = chdir - - def merge_tree( - self, - dst, - symlinks=False, - *, - update=False, - copy_function=shutil.copy2, - ignore=lambda dir, contents: [] - ): - """ - Copy entire contents of self to dst, overwriting existing - contents in dst with those in self. - - Pass ``symlinks=True`` to copy symbolic links as links. - - Accepts a ``copy_function``, similar to copytree. - - To avoid overwriting newer files, supply a copy function - wrapped in ``only_newer``. For example:: - - src.merge_tree(dst, copy_function=only_newer(shutil.copy2)) - """ - dst = self._next_class(dst) - dst.makedirs_p() - - if update: - warnings.warn( - "Update is deprecated; " "use copy_function=only_newer(shutil.copy2)", - DeprecationWarning, - stacklevel=2, - ) - copy_function = only_newer(copy_function) - - sources = self.listdir() - _ignored = ignore(self, [item.name for item in sources]) - - def ignored(item): - return item.name in _ignored - - for source in itertools.filterfalse(ignored, sources): - dest = dst / source.name - if symlinks and source.islink(): - target = source.readlink() - target.symlink(dest) - elif source.isdir(): - source.merge_tree( - dest, - symlinks=symlinks, - update=update, - copy_function=copy_function, - ignore=ignore, - ) - else: - copy_function(source, dest) - - self.copystat(dst) - - # - # --- Special stuff from os - - if hasattr(os, 'chroot'): - - def chroot(self): - """ .. seealso:: :func:`os.chroot` """ - os.chroot(self) - - if hasattr(os, 'startfile'): - - def startfile(self): - """ .. seealso:: :func:`os.startfile` """ - os.startfile(self) - return self - - # in-place re-writing, courtesy of Martijn Pieters - # http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/ - @contextlib.contextmanager - def in_place( - self, - mode='r', - buffering=-1, - encoding=None, - errors=None, - newline=None, - backup_extension=None, - ): - """ - A context in which a file may be re-written in-place with - new content. - - Yields a tuple of :samp:`({readable}, {writable})` file - objects, where `writable` replaces `readable`. - - If an exception occurs, the old file is restored, removing the - written data. - - Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only - read-only-modes are allowed. A :exc:`ValueError` is raised - on invalid modes. - - For example, to add line numbers to a file:: - - p = Path(filename) - assert p.isfile() - with p.in_place() as (reader, writer): - for number, line in enumerate(reader, 1): - writer.write('{0:3}: '.format(number))) - writer.write(line) - - Thereafter, the file at `filename` will have line numbers in it. - """ - import io - - if set(mode).intersection('wa+'): - raise ValueError('Only read-only file modes can be used') - - # move existing file to backup, create new file with same permissions - # borrowed extensively from the fileinput module - backup_fn = self + (backup_extension or os.extsep + 'bak') - try: - os.unlink(backup_fn) - except os.error: - pass - os.rename(self, backup_fn) - readable = io.open( - backup_fn, - mode, - buffering=buffering, - encoding=encoding, - errors=errors, - newline=newline, - ) - try: - perm = os.fstat(readable.fileno()).st_mode - except OSError: - writable = open( - self, - 'w' + mode.replace('r', ''), - buffering=buffering, - encoding=encoding, - errors=errors, - newline=newline, - ) - else: - os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC - if hasattr(os, 'O_BINARY'): - os_mode |= os.O_BINARY - fd = os.open(self, os_mode, perm) - writable = io.open( - fd, - "w" + mode.replace('r', ''), - buffering=buffering, - encoding=encoding, - errors=errors, - newline=newline, - ) - try: - if hasattr(os, 'chmod'): - os.chmod(self, perm) - except OSError: - pass - try: - yield readable, writable - except Exception: - # move backup back - readable.close() - writable.close() - try: - os.unlink(self) - except os.error: - pass - os.rename(backup_fn, self) - raise - else: - readable.close() - writable.close() - finally: - try: - os.unlink(backup_fn) - except os.error: - pass - - @ClassProperty - @classmethod - def special(cls): - """ - Return a SpecialResolver object suitable referencing a suitable - directory for the relevant platform for the given - type of content. - - For example, to get a user config directory, invoke: - - dir = Path.special().user.config - - Uses the `appdirs - `_ to resolve - the paths in a platform-friendly way. - - To create a config directory for 'My App', consider: - - dir = Path.special("My App").user.config.makedirs_p() - - If the ``appdirs`` module is not installed, invocation - of special will raise an ImportError. - """ - return functools.partial(SpecialResolver, cls) - - -class DirectoryNotEmpty(OSError): - @staticmethod - @contextlib.contextmanager - def translate(): - try: - yield - except OSError as exc: - if exc.errno == errno.ENOTEMPTY: - raise DirectoryNotEmpty(*exc.args) from exc - raise - - -def only_newer(copy_func): - """ - Wrap a copy function (like shutil.copy2) to return - the dst if it's newer than the source. - """ - - @functools.wraps(copy_func) - def wrapper(src, dst, *args, **kwargs): - is_newer_dst = dst.exists() and dst.getmtime() >= src.getmtime() - if is_newer_dst: - return dst - return copy_func(src, dst, *args, **kwargs) - - return wrapper - - -class SpecialResolver(object): - class ResolverScope: - def __init__(self, paths, scope): - self.paths = paths - self.scope = scope - - def __getattr__(self, class_): - return self.paths.get_dir(self.scope, class_) - - def __init__(self, path_class, *args, **kwargs): - appdirs = importlib.import_module('appdirs') - - # let appname default to None until - # https://github.com/ActiveState/appdirs/issues/55 is solved. - not args and kwargs.setdefault('appname', None) - - vars(self).update( - path_class=path_class, wrapper=appdirs.AppDirs(*args, **kwargs) - ) - - def __getattr__(self, scope): - return self.ResolverScope(self, scope) - - def get_dir(self, scope, class_): - """ - Return the callable function from appdirs, but with the - result wrapped in self.path_class - """ - prop_name = '{scope}_{class_}_dir'.format(**locals()) - value = getattr(self.wrapper, prop_name) - MultiPath = Multi.for_class(self.path_class) - return MultiPath.detect(value) - - -class Multi: - """ - A mix-in for a Path which may contain multiple Path separated by pathsep. - """ - - @classmethod - def for_class(cls, path_cls): - name = 'Multi' + path_cls.__name__ - return type(name, (cls, path_cls), {}) - - @classmethod - def detect(cls, input): - if os.pathsep not in input: - cls = cls._next_class - return cls(input) - - def __iter__(self): - return iter(map(self._next_class, self.split(os.pathsep))) - - @ClassProperty - @classmethod - def _next_class(cls): - """ - Multi-subclasses should use the parent class - """ - return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi)) - - -class TempDir(Path): - """ - A temporary directory via :func:`tempfile.mkdtemp`, and - constructed with the same parameters that you can use - as a context manager. - - Example:: - - with TempDir() as d: - # do stuff with the Path object "d" - - # here the directory is deleted automatically - - .. seealso:: :func:`tempfile.mkdtemp` - """ - - @ClassProperty - @classmethod - def _next_class(cls): - return Path - - def __new__(cls, *args, **kwargs): - dirname = tempfile.mkdtemp(*args, **kwargs) - return super(TempDir, cls).__new__(cls, dirname) - - def __init__(self, *args, **kwargs): - pass - - def __enter__(self): - # TempDir should return a Path version of itself and not itself - # so that a second context manager does not create a second - # temporary directory, but rather changes CWD to the location - # of the temporary directory. - return self._next_class(self) - - def __exit__(self, exc_type, exc_value, traceback): - if not exc_value: - self.rmtree() - - -# For backwards compatibility. -tempdir = TempDir - - -def _multi_permission_mask(mode): - """ - Support multiple, comma-separated Unix chmod symbolic modes. - - >>> _multi_permission_mask('a=r,u+w')(0) == 0o644 - True - """ - - def compose(f, g): - return lambda *args, **kwargs: g(f(*args, **kwargs)) - - return functools.reduce(compose, map(_permission_mask, mode.split(','))) - - -def _permission_mask(mode): - """ - Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function - suitable for applying to a mask to affect that change. - - >>> mask = _permission_mask('ugo+rwx') - >>> mask(0o554) == 0o777 - True - - >>> _permission_mask('go-x')(0o777) == 0o766 - True - - >>> _permission_mask('o-x')(0o445) == 0o444 - True - - >>> _permission_mask('a+x')(0) == 0o111 - True - - >>> _permission_mask('a=rw')(0o057) == 0o666 - True - - >>> _permission_mask('u=x')(0o666) == 0o166 - True - - >>> _permission_mask('g=')(0o157) == 0o107 - True - """ - # parse the symbolic mode - parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) - if not parsed: - raise ValueError("Unrecognized symbolic mode", mode) - - # generate a mask representing the specified permission - spec_map = dict(r=4, w=2, x=1) - specs = (spec_map[perm] for perm in parsed.group('what')) - spec = functools.reduce(operator.or_, specs, 0) - - # now apply spec to each subject in who - shift_map = dict(u=6, g=3, o=0) - who = parsed.group('who').replace('a', 'ugo') - masks = (spec << shift_map[subj] for subj in who) - mask = functools.reduce(operator.or_, masks) - - op = parsed.group('op') - - # if op is -, invert the mask - if op == '-': - mask ^= 0o777 - - # if op is =, retain extant values for unreferenced subjects - if op == '=': - masks = (0o7 << shift_map[subj] for subj in who) - retain = functools.reduce(operator.or_, masks) ^ 0o777 - - op_map = { - '+': operator.or_, - '-': operator.and_, - '=': lambda mask, target: target & retain ^ mask, - } - return functools.partial(op_map[op], mask) - - -class CaseInsensitivePattern(matchers.CaseInsensitive): - def __init__(self, value): - warnings.warn( - "Use matchers.CaseInsensitive instead", DeprecationWarning, stacklevel=2 - ) - super(CaseInsensitivePattern, self).__init__(value) - - -class FastPath(Path): - def __init__(self, *args, **kwargs): - warnings.warn( - "Use Path, as FastPath no longer holds any advantage", - DeprecationWarning, - stacklevel=2, - ) - super(FastPath, self).__init__(*args, **kwargs) diff --git a/path/matchers.py b/path/matchers.py deleted file mode 100644 index 053f25a8..00000000 --- a/path/matchers.py +++ /dev/null @@ -1,59 +0,0 @@ -import ntpath -import fnmatch - - -def load(param): - """ - If the supplied parameter is a string, assum it's a simple - pattern. - """ - return ( - Pattern(param) - if isinstance(param, str) - else param - if param is not None - else Null() - ) - - -class Base(object): - pass - - -class Null(Base): - def __call__(self, path): - return True - - -class Pattern(Base): - def __init__(self, pattern): - self.pattern = pattern - - def get_pattern(self, normcase): - try: - return self._pattern - except AttributeError: - pass - self._pattern = normcase(self.pattern) - return self._pattern - - def __call__(self, path): - normcase = getattr(self, 'normcase', path.module.normcase) - pattern = self.get_pattern(normcase) - return fnmatch.fnmatchcase(normcase(path.name), pattern) - - -class CaseInsensitive(Pattern): - """ - A Pattern with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, - :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. - - For example, to get all files ending in .py, .Py, .pY, or .PY in the - current directory:: - - from path import Path, matchers - Path('.').files(matchers.CaseInsensitive('*.py')) - """ - - normcase = staticmethod(ntpath.normcase) diff --git a/setup.cfg b/setup.cfg index 9ebd4a39..17ab190c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = find: include_package_data = true python_requires = >=3.5 install_requires = - importlib_metadata >= 0.5; python_version < "3.8" + path < 13.1 setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] diff --git a/test_path.py b/test_path.py deleted file mode 100644 index 5519cdb2..00000000 --- a/test_path.py +++ /dev/null @@ -1,1255 +0,0 @@ -""" -Tests for the path module. - -This suite runs on Linux, macOS, and Windows. To extend the -platform support, just add appropriate pathnames for your -platform (os.name) in each place where the p() function is called. -Then report the result. If you can't get the test to run at all on -your platform, there's probably a bug -- please report the issue -in the issue tracker. - -TestScratchDir.test_touch() takes a while to run. It sleeps a few -seconds to allow some time to pass between calls to check the modify -time on files. -""" - -import codecs -import os -import sys -import shutil -import time -import types -import ntpath -import posixpath -import textwrap -import platform -import importlib -import operator -import datetime -import subprocess -import re - -import pytest -import packaging.version - -import path -from path import TempDir -from path import matchers -from path import SpecialResolver -from path import Multi - -Path = None - - -def p(**choices): - """ Choose a value from several possible values, based on os.name """ - return choices[os.name] - - -@pytest.fixture(autouse=True, params=[path.Path]) -def path_class(request, monkeypatch): - """ - Invoke tests on any number of Path classes. - """ - monkeypatch.setitem(globals(), 'Path', request.param) - - -def mac_version(target, comparator=operator.ge): - """ - Return True if on a Mac whose version passes the comparator. - """ - current_ver = packaging.version.parse(platform.mac_ver()[0]) - target_ver = packaging.version.parse(target) - return platform.system() == 'Darwin' and comparator(current_ver, target_ver) - - -class TestBasics: - def test_relpath(self): - root = Path(p(nt='C:\\', posix='/')) - foo = root / 'foo' - quux = foo / 'quux' - bar = foo / 'bar' - boz = bar / 'Baz' / 'Boz' - up = Path(os.pardir) - - # basics - assert root.relpathto(boz) == Path('foo') / 'bar' / 'Baz' / 'Boz' - assert bar.relpathto(boz) == Path('Baz') / 'Boz' - assert quux.relpathto(boz) == up / 'bar' / 'Baz' / 'Boz' - assert boz.relpathto(quux) == up / up / up / 'quux' - assert boz.relpathto(bar) == up / up - - # Path is not the first element in concatenation - assert root.relpathto(boz) == 'foo' / Path('bar') / 'Baz' / 'Boz' - - # x.relpathto(x) == curdir - assert root.relpathto(root) == os.curdir - assert boz.relpathto(boz) == os.curdir - # Make sure case is properly noted (or ignored) - assert boz.relpathto(boz.normcase()) == os.curdir - - # relpath() - cwd = Path(os.getcwd()) - assert boz.relpath() == cwd.relpathto(boz) - - if os.name == 'nt': - # Check relpath across drives. - d = Path('D:\\') - assert d.relpathto(boz) == boz - - def test_construction_from_none(self): - """ - - """ - try: - Path(None) - except TypeError: - pass - else: - raise Exception("DID NOT RAISE") - - def test_construction_from_int(self): - """ - Path class will construct a path as a string of the number - """ - assert Path(1) == '1' - - def test_string_compatibility(self): - """ Test compatibility with ordinary strings. """ - x = Path('xyzzy') - assert x == 'xyzzy' - assert x == str('xyzzy') - - # sorting - items = [Path('fhj'), Path('fgh'), 'E', Path('d'), 'A', Path('B'), 'c'] - items.sort() - assert items == ['A', 'B', 'E', 'c', 'd', 'fgh', 'fhj'] - - # Test p1/p1. - p1 = Path("foo") - p2 = Path("bar") - assert p1 / p2 == p(nt='foo\\bar', posix='foo/bar') - - def test_properties(self): - # Create sample path object. - f = p( - nt='C:\\Program Files\\Python\\Lib\\xyzzy.py', - posix='/usr/local/python/lib/xyzzy.py', - ) - f = Path(f) - - # .parent - nt_lib = 'C:\\Program Files\\Python\\Lib' - posix_lib = '/usr/local/python/lib' - expected = p(nt=nt_lib, posix=posix_lib) - assert f.parent == expected - - # .name - assert f.name == 'xyzzy.py' - assert f.parent.name == p(nt='Lib', posix='lib') - - # .ext - assert f.ext == '.py' - assert f.parent.ext == '' - - # .drive - assert f.drive == p(nt='C:', posix='') - - def test_methods(self): - # .abspath() - assert Path(os.curdir).abspath() == os.getcwd() - - # .getcwd() - cwd = Path.getcwd() - assert isinstance(cwd, Path) - assert cwd == os.getcwd() - - def test_UNC(self): - if hasattr(os.path, 'splitunc'): - p = Path(r'\\python1\share1\dir1\file1.txt') - assert p.uncshare == r'\\python1\share1' - assert p.splitunc() == os.path.splitunc(str(p)) - - def test_explicit_module(self): - """ - The user may specify an explicit path module to use. - """ - nt_ok = Path.using_module(ntpath)(r'foo\bar\baz') - posix_ok = Path.using_module(posixpath)(r'foo/bar/baz') - posix_wrong = Path.using_module(posixpath)(r'foo\bar\baz') - - assert nt_ok.dirname() == r'foo\bar' - assert posix_ok.dirname() == r'foo/bar' - assert posix_wrong.dirname() == '' - - assert nt_ok / 'quux' == r'foo\bar\baz\quux' - assert posix_ok / 'quux' == r'foo/bar/baz/quux' - - def test_explicit_module_classes(self): - """ - Multiple calls to path.using_module should produce the same class. - """ - nt_path = Path.using_module(ntpath) - assert nt_path is Path.using_module(ntpath) - assert nt_path.__name__ == 'Path_ntpath' - - def test_joinpath_on_instance(self): - res = Path('foo') - foo_bar = res.joinpath('bar') - assert foo_bar == p(nt='foo\\bar', posix='foo/bar') - - def test_joinpath_to_nothing(self): - res = Path('foo') - assert res.joinpath() == res - - def test_joinpath_on_class(self): - "Construct a path from a series of strings" - foo_bar = Path.joinpath('foo', 'bar') - assert foo_bar == p(nt='foo\\bar', posix='foo/bar') - - def test_joinpath_fails_on_empty(self): - "It doesn't make sense to join nothing at all" - try: - Path.joinpath() - except TypeError: - pass - else: - raise Exception("did not raise") - - def test_joinpath_returns_same_type(self): - path_posix = Path.using_module(posixpath) - res = path_posix.joinpath('foo') - assert isinstance(res, path_posix) - res2 = res.joinpath('bar') - assert isinstance(res2, path_posix) - assert res2 == 'foo/bar' - - -class TestPerformance: - @staticmethod - def get_command_time(cmd): - args = [sys.executable, '-m', 'timeit', '-n', '1', '-r', '1', '-u', 'usec'] + [ - cmd - ] - res = subprocess.check_output(args, universal_newlines=True) - dur = re.search(r'(\d+) usec per loop', res).group(1) - return datetime.timedelta(microseconds=int(dur)) - - def test_import_time(self, monkeypatch): - """ - Import of path.py should take less than some limit. - - Run tests in a subprocess to isolate from test suite overhead. - """ - limit = datetime.timedelta(milliseconds=20) - baseline = self.get_command_time('pass') - measure = self.get_command_time('import path') - duration = measure - baseline - assert duration < limit - - -class TestSelfReturn: - """ - Some methods don't necessarily return any value (e.g. makedirs, - makedirs_p, rename, mkdir, touch, chroot). These methods should return - self anyhow to allow methods to be chained. - """ - - def test_makedirs_p(self, tmpdir): - """ - Path('foo').makedirs_p() == Path('foo') - """ - p = Path(tmpdir) / "newpath" - ret = p.makedirs_p() - assert p == ret - - def test_makedirs_p_extant(self, tmpdir): - p = Path(tmpdir) - ret = p.makedirs_p() - assert p == ret - - def test_rename(self, tmpdir): - p = Path(tmpdir) / "somefile" - p.touch() - target = Path(tmpdir) / "otherfile" - ret = p.rename(target) - assert target == ret - - def test_mkdir(self, tmpdir): - p = Path(tmpdir) / "newdir" - ret = p.mkdir() - assert p == ret - - def test_touch(self, tmpdir): - p = Path(tmpdir) / "empty file" - ret = p.touch() - assert p == ret - - -class TestScratchDir: - """ - Tests that run in a temporary directory (does not test TempDir class) - """ - - def test_context_manager(self, tmpdir): - """Can be used as context manager for chdir.""" - d = Path(tmpdir) - subdir = d / 'subdir' - subdir.makedirs() - old_dir = os.getcwd() - with subdir: - assert os.getcwd() == os.path.realpath(subdir) - assert os.getcwd() == old_dir - - def test_touch(self, tmpdir): - # NOTE: This test takes a long time to run (~10 seconds). - # It sleeps several seconds because on Windows, the resolution - # of a file's mtime and ctime is about 2 seconds. - # - # atime isn't tested because on Windows the resolution of atime - # is something like 24 hours. - - threshold = 1 - - d = Path(tmpdir) - f = d / 'test.txt' - t0 = time.time() - threshold - f.touch() - t1 = time.time() + threshold - - assert f.exists() - assert f.isfile() - assert f.size == 0 - assert t0 <= f.mtime <= t1 - if hasattr(os.path, 'getctime'): - ct = f.ctime - assert t0 <= ct <= t1 - - time.sleep(threshold * 2) - fobj = open(f, 'ab') - fobj.write('some bytes'.encode('utf-8')) - fobj.close() - - time.sleep(threshold * 2) - t2 = time.time() - threshold - f.touch() - t3 = time.time() + threshold - - assert t0 <= t1 < t2 <= t3 # sanity check - - assert f.exists() - assert f.isfile() - assert f.size == 10 - assert t2 <= f.mtime <= t3 - if hasattr(os.path, 'getctime'): - ct2 = f.ctime - if os.name == 'nt': - # On Windows, "ctime" is CREATION time - assert ct == ct2 - assert ct2 < t2 - else: - assert ( - # ctime is unchanged - ct == ct2 - or - # ctime is approximately the mtime - ct2 == pytest.approx(f.mtime, 0.001) - ) - - def test_listing(self, tmpdir): - d = Path(tmpdir) - assert d.listdir() == [] - - f = 'testfile.txt' - af = d / f - assert af == os.path.join(d, f) - af.touch() - try: - assert af.exists() - - assert d.listdir() == [af] - - # .glob() - assert d.glob('testfile.txt') == [af] - assert d.glob('test*.txt') == [af] - assert d.glob('*.txt') == [af] - assert d.glob('*txt') == [af] - assert d.glob('*') == [af] - assert d.glob('*.html') == [] - assert d.glob('testfile') == [] - - # .iglob matches .glob but as an iterator. - assert list(d.iglob('*')) == d.glob('*') - assert isinstance(d.iglob('*'), types.GeneratorType) - - finally: - af.remove() - - # Try a test with 20 files - files = [d / ('%d.txt' % i) for i in range(20)] - for f in files: - fobj = open(f, 'w') - fobj.write('some text\n') - fobj.close() - try: - files2 = d.listdir() - files.sort() - files2.sort() - assert files == files2 - finally: - for f in files: - try: - f.remove() - except Exception: - pass - - @pytest.mark.xfail(mac_version('10.13'), reason="macOS disallows invalid encodings") - @pytest.mark.xfail( - platform.system() == 'Windows', reason="Can't write latin characters. See #133" - ) - def test_listdir_other_encoding(self, tmpdir): - """ - Some filesystems allow non-character sequences in path names. - ``.listdir`` should still function in this case. - See issue #61 for details. - """ - assert Path(tmpdir).listdir() == [] - tmpdir_bytes = str(tmpdir).encode('ascii') - - filename = 'r\xe9\xf1emi'.encode('latin-1') - pathname = os.path.join(tmpdir_bytes, filename) - with open(pathname, 'wb'): - pass - # first demonstrate that os.listdir works - assert os.listdir(tmpdir_bytes) - - # now try with path.py - results = Path(tmpdir).listdir() - assert len(results) == 1 - res, = results - assert isinstance(res, Path) - # OS X seems to encode the bytes in the filename as %XX characters. - if platform.system() == 'Darwin': - assert res.basename() == 'r%E9%F1emi' - return - assert len(res.basename()) == len(filename) - - def test_makedirs(self, tmpdir): - d = Path(tmpdir) - - # Placeholder file so that when removedirs() is called, - # it doesn't remove the temporary directory itself. - tempf = d / 'temp.txt' - tempf.touch() - try: - foo = d / 'foo' - boz = foo / 'bar' / 'baz' / 'boz' - boz.makedirs() - try: - assert boz.isdir() - finally: - boz.removedirs() - assert not foo.exists() - assert d.exists() - - foo.mkdir(0o750) - boz.makedirs(0o700) - try: - assert boz.isdir() - finally: - boz.removedirs() - assert not foo.exists() - assert d.exists() - finally: - os.remove(tempf) - - def assertSetsEqual(self, a, b): - ad = {} - - for i in a: - ad[i] = None - - bd = {} - - for i in b: - bd[i] = None - - assert ad == bd - - def test_shutil(self, tmpdir): - # Note: This only tests the methods exist and do roughly what - # they should, neglecting the details as they are shutil's - # responsibility. - - d = Path(tmpdir) - testDir = d / 'testdir' - testFile = testDir / 'testfile.txt' - testA = testDir / 'A' - testCopy = testA / 'testcopy.txt' - testLink = testA / 'testlink.txt' - testB = testDir / 'B' - testC = testB / 'C' - testCopyOfLink = testC / testA.relpathto(testLink) - - # Create test dirs and a file - testDir.mkdir() - testA.mkdir() - testB.mkdir() - - f = open(testFile, 'w') - f.write('x' * 10000) - f.close() - - # Test simple file copying. - testFile.copyfile(testCopy) - assert testCopy.isfile() - assert testFile.bytes() == testCopy.bytes() - - # Test copying into a directory. - testCopy2 = testA / testFile.name - testFile.copy(testA) - assert testCopy2.isfile() - assert testFile.bytes() == testCopy2.bytes() - - # Make a link for the next test to use. - if hasattr(os, 'symlink'): - testFile.symlink(testLink) - else: - testFile.copy(testLink) # fallback - - # Test copying directory tree. - testA.copytree(testC) - assert testC.isdir() - self.assertSetsEqual( - testC.listdir(), - [testC / testCopy.name, testC / testFile.name, testCopyOfLink], - ) - assert not testCopyOfLink.islink() - - # Clean up for another try. - testC.rmtree() - assert not testC.exists() - - # Copy again, preserving symlinks. - testA.copytree(testC, True) - assert testC.isdir() - self.assertSetsEqual( - testC.listdir(), - [testC / testCopy.name, testC / testFile.name, testCopyOfLink], - ) - if hasattr(os, 'symlink'): - assert testCopyOfLink.islink() - assert testCopyOfLink.readlink() == testFile - - # Clean up. - testDir.rmtree() - assert not testDir.exists() - self.assertList(d.listdir(), []) - - def assertList(self, listing, expected): - assert sorted(listing) == sorted(expected) - - def test_patterns(self, tmpdir): - d = Path(tmpdir) - names = ['x.tmp', 'x.xtmp', 'x2g', 'x22', 'x.txt'] - dirs = [d, d / 'xdir', d / 'xdir.tmp', d / 'xdir.tmp' / 'xsubdir'] - - for e in dirs: - if not e.isdir(): - e.makedirs() - - for name in names: - (e / name).touch() - self.assertList(d.listdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) - self.assertList(d.files('*.tmp'), [d / 'x.tmp']) - self.assertList(d.dirs('*.tmp'), [d / 'xdir.tmp']) - self.assertList( - d.walk(), [e for e in dirs if e != d] + [e / n for e in dirs for n in names] - ) - self.assertList(d.walk('*.tmp'), [e / 'x.tmp' for e in dirs] + [d / 'xdir.tmp']) - self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) - self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) - - def test_unicode(self, tmpdir): - d = Path(tmpdir) - p = d / 'unicode.txt' - - def test(enc): - """ Test that path works with the specified encoding, - which must be capable of representing the entire range of - Unicode codepoints. - """ - - given = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\r\n' - '\u0d0a\u0a0d\u0d15\u0a15\x85' - '\u0d0a\u0a0d\u0d15\u0a15\u2028' - '\r' - 'hanging' - ) - clean = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\n' - 'hanging' - ) - givenLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\r\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\x85'), - ('\u0d0a\u0a0d\u0d15\u0a15\u2028'), - ('\r'), - ('hanging'), - ] - expectedLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\n'), - ('hanging'), - ] - expectedLines2 = [ - ('Hello world'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - (''), - ('hanging'), - ] - - # write bytes manually to file - f = codecs.open(p, 'w', enc) - f.write(given) - f.close() - - # test all 3 path read-fully functions, including - # path.lines() in unicode mode. - assert p.bytes() == given.encode(enc) - assert p.text(enc) == clean - assert p.lines(enc) == expectedLines - assert p.lines(enc, retain=False) == expectedLines2 - - # If this is UTF-16, that's enough. - # The rest of these will unfortunately fail because append=True - # mode causes an extra BOM to be written in the middle of the file. - # UTF-16 is the only encoding that has this problem. - if enc == 'UTF-16': - return - - # Write Unicode to file using path.write_text(). - # This test doesn't work with a hanging line. - cleanNoHanging = clean + '\n' - - p.write_text(cleanNoHanging, enc) - p.write_text(cleanNoHanging, enc, append=True) - # Check the result. - expectedBytes = 2 * cleanNoHanging.replace('\n', os.linesep).encode(enc) - expectedLinesNoHanging = expectedLines[:] - expectedLinesNoHanging[-1] += '\n' - assert p.bytes() == expectedBytes - assert p.text(enc) == 2 * cleanNoHanging - assert p.lines(enc) == 2 * expectedLinesNoHanging - assert p.lines(enc, retain=False) == 2 * expectedLines2 - - # Write Unicode to file using path.write_lines(). - # The output in the file should be exactly the same as last time. - p.write_lines(expectedLines, enc) - p.write_lines(expectedLines2, enc, append=True) - # Check the result. - assert p.bytes() == expectedBytes - - # Now: same test, but using various newline sequences. - # If linesep is being properly applied, these will be converted - # to the platform standard newline sequence. - p.write_lines(givenLines, enc) - p.write_lines(givenLines, enc, append=True) - # Check the result. - assert p.bytes() == expectedBytes - - # Same test, using newline sequences that are different - # from the platform default. - def testLinesep(eol): - p.write_lines(givenLines, enc, linesep=eol) - p.write_lines(givenLines, enc, linesep=eol, append=True) - expected = 2 * cleanNoHanging.replace('\n', eol).encode(enc) - assert p.bytes() == expected - - testLinesep('\n') - testLinesep('\r') - testLinesep('\r\n') - testLinesep('\x0d\x85') - - # Again, but with linesep=None. - p.write_lines(givenLines, enc, linesep=None) - p.write_lines(givenLines, enc, linesep=None, append=True) - # Check the result. - expectedBytes = 2 * given.encode(enc) - assert p.bytes() == expectedBytes - assert p.text(enc) == 2 * clean - expectedResultLines = expectedLines[:] - expectedResultLines[-1] += expectedLines[0] - expectedResultLines += expectedLines[1:] - assert p.lines(enc) == expectedResultLines - - test('UTF-8') - test('UTF-16BE') - test('UTF-16LE') - test('UTF-16') - - def test_chunks(self, tmpdir): - p = (TempDir() / 'test.txt').touch() - txt = "0123456789" - size = 5 - p.write_text(txt) - for i, chunk in enumerate(p.chunks(size)): - assert chunk == txt[i * size : i * size + size] - - assert i == len(txt) / size - 1 - - @pytest.mark.skipif(not hasattr(os.path, 'samefile'), reason="samefile not present") - def test_samefile(self, tmpdir): - f1 = (TempDir() / '1.txt').touch() - f1.write_text('foo') - f2 = (TempDir() / '2.txt').touch() - f1.write_text('foo') - f3 = (TempDir() / '3.txt').touch() - f1.write_text('bar') - f4 = TempDir() / '4.txt' - f1.copyfile(f4) - - assert os.path.samefile(f1, f2) == f1.samefile(f2) - assert os.path.samefile(f1, f3) == f1.samefile(f3) - assert os.path.samefile(f1, f4) == f1.samefile(f4) - assert os.path.samefile(f1, f1) == f1.samefile(f1) - - def test_rmtree_p(self, tmpdir): - d = Path(tmpdir) - sub = d / 'subfolder' - sub.mkdir() - (sub / 'afile').write_text('something') - sub.rmtree_p() - assert not sub.exists() - try: - sub.rmtree_p() - except OSError: - self.fail( - "Calling `rmtree_p` on non-existent directory " - "should not raise an exception." - ) - - def test_rmdir_p_exists(self, tmpdir): - """ - Invocation of rmdir_p on an existant directory should - remove the directory. - """ - d = Path(tmpdir) - sub = d / 'subfolder' - sub.mkdir() - sub.rmdir_p() - assert not sub.exists() - - def test_rmdir_p_nonexistent(self, tmpdir): - """ - A non-existent file should not raise an exception. - """ - d = Path(tmpdir) - sub = d / 'subfolder' - assert not sub.exists() - sub.rmdir_p() - - def test_rmdir_p_sub_sub_dir(self, tmpdir): - """ - A non-empty folder should not raise an exception. - """ - d = Path(tmpdir) - sub = d / 'subfolder' - sub.mkdir() - subsub = sub / 'subfolder' - subsub.mkdir() - - sub.rmdir_p() - - -class TestMergeTree: - @pytest.fixture(autouse=True) - def testing_structure(self, tmpdir): - self.test_dir = Path(tmpdir) - self.subdir_a = self.test_dir / 'A' - self.test_file = self.subdir_a / 'testfile.txt' - self.test_link = self.subdir_a / 'testlink.txt' - self.subdir_b = self.test_dir / 'B' - - self.subdir_a.mkdir() - self.subdir_b.mkdir() - - with open(self.test_file, 'w') as f: - f.write('x' * 10000) - - if hasattr(os, 'symlink'): - self.test_file.symlink(self.test_link) - else: - self.test_file.copy(self.test_link) - - def check_link(self): - target = Path(self.subdir_b / self.test_link.name) - check = target.islink if hasattr(os, 'symlink') else target.isfile - assert check() - - def test_with_nonexisting_dst_kwargs(self): - self.subdir_a.merge_tree(self.subdir_b, symlinks=True) - assert self.subdir_b.isdir() - expected = set( - (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) - ) - assert set(self.subdir_b.listdir()) == expected - self.check_link() - - def test_with_nonexisting_dst_args(self): - self.subdir_a.merge_tree(self.subdir_b, True) - assert self.subdir_b.isdir() - expected = set( - (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) - ) - assert set(self.subdir_b.listdir()) == expected - self.check_link() - - def test_with_existing_dst(self): - self.subdir_b.rmtree() - self.subdir_a.copytree(self.subdir_b, True) - - self.test_link.remove() - test_new = self.subdir_a / 'newfile.txt' - test_new.touch() - with open(self.test_file, 'w') as f: - f.write('x' * 5000) - - self.subdir_a.merge_tree(self.subdir_b, True) - - assert self.subdir_b.isdir() - expected = set( - ( - self.subdir_b / self.test_file.name, - self.subdir_b / self.test_link.name, - self.subdir_b / test_new.name, - ) - ) - assert set(self.subdir_b.listdir()) == expected - self.check_link() - assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 - - def test_copytree_parameters(self): - """ - merge_tree should accept parameters to copytree, such as 'ignore' - """ - ignore = shutil.ignore_patterns('testlink*') - self.subdir_a.merge_tree(self.subdir_b, ignore=ignore) - - assert self.subdir_b.isdir() - assert self.subdir_b.listdir() == [self.subdir_b / self.test_file.name] - - def test_only_newer(self): - """ - merge_tree should accept a copy_function in which only - newer files are copied and older files do not overwrite - newer copies in the dest. - """ - target = self.subdir_b / 'testfile.txt' - target.write_text('this is newer') - self.subdir_a.merge_tree( - self.subdir_b, copy_function=path.only_newer(shutil.copy2) - ) - assert target.text() == 'this is newer' - - -class TestChdir: - def test_chdir_or_cd(self, tmpdir): - """ tests the chdir or cd method """ - d = Path(str(tmpdir)) - cwd = d.getcwd() - - # ensure the cwd isn't our tempdir - assert str(d) != str(cwd) - # now, we're going to chdir to tempdir - d.chdir() - - # we now ensure that our cwd is the tempdir - assert str(d.getcwd()) == str(tmpdir) - # we're resetting our path - d = Path(cwd) - - # we ensure that our cwd is still set to tempdir - assert str(d.getcwd()) == str(tmpdir) - - # we're calling the alias cd method - d.cd() - # now, we ensure cwd isn'r tempdir - assert str(d.getcwd()) == str(cwd) - assert str(d.getcwd()) != str(tmpdir) - - -class TestSubclass: - def test_subclass_produces_same_class(self): - """ - When operations are invoked on a subclass, they should produce another - instance of that subclass. - """ - - class PathSubclass(Path): - pass - - p = PathSubclass('/foo') - subdir = p / 'bar' - assert isinstance(subdir, PathSubclass) - - -class TestTempDir: - def test_constructor(self): - """ - One should be able to readily construct a temporary directory - """ - d = TempDir() - assert isinstance(d, path.Path) - assert d.exists() - assert d.isdir() - d.rmdir() - assert not d.exists() - - def test_next_class(self): - """ - It should be possible to invoke operations on a TempDir and get - Path classes. - """ - d = TempDir() - sub = d / 'subdir' - assert isinstance(sub, path.Path) - d.rmdir() - - def test_context_manager(self): - """ - One should be able to use a TempDir object as a context, which will - clean up the contents after. - """ - d = TempDir() - res = d.__enter__() - assert res == path.Path(d) - (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', TempDir) - d.__exit__(None, None, None) - assert not d.exists() - - def test_context_manager_exception(self): - """ - The context manager will not clean up if an exception occurs. - """ - d = TempDir() - d.__enter__() - (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', TempDir) - d.__exit__(TypeError, TypeError('foo'), None) - assert d.exists() - - def test_context_manager_using_with(self): - """ - The context manager will allow using the with keyword and - provide a temporry directory that will be deleted after that. - """ - - with TempDir() as d: - assert d.isdir() - assert not d.isdir() - - -class TestUnicode: - @pytest.fixture(autouse=True) - def unicode_name_in_tmpdir(self, tmpdir): - # build a snowman (dir) in the temporary directory - Path(tmpdir).joinpath('☃').mkdir() - - def test_walkdirs_with_unicode_name(self, tmpdir): - for res in Path(tmpdir).walkdirs(): - pass - - -class TestPatternMatching: - def test_fnmatch_simple(self): - p = Path('FooBar') - assert p.fnmatch('Foo*') - assert p.fnmatch('Foo[ABC]ar') - - def test_fnmatch_custom_mod(self): - p = Path('FooBar') - p.module = ntpath - assert p.fnmatch('foobar') - assert p.fnmatch('FOO[ABC]AR') - - def test_fnmatch_custom_normcase(self): - def normcase(path): - return path.upper() - - p = Path('FooBar') - assert p.fnmatch('foobar', normcase=normcase) - assert p.fnmatch('FOO[ABC]AR', normcase=normcase) - - def test_listdir_simple(self): - p = Path('.') - assert len(p.listdir()) == len(os.listdir('.')) - - def test_listdir_empty_pattern(self): - p = Path('.') - assert p.listdir('') == [] - - def test_listdir_patterns(self, tmpdir): - p = Path(tmpdir) - (p / 'sub').mkdir() - (p / 'File').touch() - assert p.listdir('s*') == [p / 'sub'] - assert len(p.listdir('*')) == 2 - - def test_listdir_custom_module(self, tmpdir): - """ - Listdir patterns should honor the case sensitivity of the path module - used by that Path class. - """ - always_unix = Path.using_module(posixpath) - p = always_unix(tmpdir) - (p / 'sub').mkdir() - (p / 'File').touch() - assert p.listdir('S*') == [] - - always_win = Path.using_module(ntpath) - p = always_win(tmpdir) - assert p.listdir('S*') == [p / 'sub'] - assert p.listdir('f*') == [p / 'File'] - - def test_listdir_case_insensitive(self, tmpdir): - """ - Listdir patterns should honor the case sensitivity of the path module - used by that Path class. - """ - p = Path(tmpdir) - (p / 'sub').mkdir() - (p / 'File').touch() - assert p.listdir(matchers.CaseInsensitive('S*')) == [p / 'sub'] - assert p.listdir(matchers.CaseInsensitive('f*')) == [p / 'File'] - assert p.files(matchers.CaseInsensitive('S*')) == [] - assert p.dirs(matchers.CaseInsensitive('f*')) == [] - - def test_walk_case_insensitive(self, tmpdir): - p = Path(tmpdir) - (p / 'sub1' / 'foo').makedirs_p() - (p / 'sub2' / 'foo').makedirs_p() - (p / 'sub1' / 'foo' / 'bar.Txt').touch() - (p / 'sub2' / 'foo' / 'bar.TXT').touch() - (p / 'sub2' / 'foo' / 'bar.txt.bz2').touch() - files = list(p.walkfiles(matchers.CaseInsensitive('*.txt'))) - assert len(files) == 2 - assert p / 'sub2' / 'foo' / 'bar.TXT' in files - assert p / 'sub1' / 'foo' / 'bar.Txt' in files - - -@pytest.mark.skipif( - sys.version_info < (2, 6), reason="in_place requires io module in Python 2.6" -) -class TestInPlace: - reference_content = textwrap.dedent( - """ - The quick brown fox jumped over the lazy dog. - """.lstrip() - ) - reversed_content = textwrap.dedent( - """ - .god yzal eht revo depmuj xof nworb kciuq ehT - """.lstrip() - ) - alternate_content = textwrap.dedent( - """ - Lorem ipsum dolor sit amet, consectetur adipisicing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit - esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui - officia deserunt mollit anim id est laborum. - """.lstrip() - ) - - @classmethod - def create_reference(cls, tmpdir): - p = Path(tmpdir) / 'document' - with p.open('w') as stream: - stream.write(cls.reference_content) - return p - - def test_line_by_line_rewrite(self, tmpdir): - doc = self.create_reference(tmpdir) - # reverse all the text in the document, line by line - with doc.in_place() as (reader, writer): - for line in reader: - r_line = ''.join(reversed(line.strip())) + '\n' - writer.write(r_line) - with doc.open() as stream: - data = stream.read() - assert data == self.reversed_content - - def test_exception_in_context(self, tmpdir): - doc = self.create_reference(tmpdir) - with pytest.raises(RuntimeError) as exc: - with doc.in_place() as (reader, writer): - writer.write(self.alternate_content) - raise RuntimeError("some error") - assert "some error" in str(exc.value) - with doc.open() as stream: - data = stream.read() - assert 'Lorem' not in data - assert 'lazy dog' in data - - -class TestSpecialPaths: - @pytest.fixture(autouse=True, scope='class') - def appdirs_installed(cls): - pytest.importorskip('appdirs') - - @pytest.fixture - def feign_linux(self, monkeypatch): - monkeypatch.setattr("platform.system", lambda: "Linux") - monkeypatch.setattr("sys.platform", "linux") - monkeypatch.setattr("os.pathsep", ":") - # remove any existing import of appdirs, as it sets up some - # state during import. - sys.modules.pop('appdirs') - - def test_basic_paths(self): - appdirs = importlib.import_module('appdirs') - - expected = appdirs.user_config_dir() - assert SpecialResolver(Path).user.config == expected - - expected = appdirs.site_config_dir() - assert SpecialResolver(Path).site.config == expected - - expected = appdirs.user_config_dir('My App', 'Me') - assert SpecialResolver(Path, 'My App', 'Me').user.config == expected - - def test_unix_paths(self, tmpdir, monkeypatch, feign_linux): - fake_config = tmpdir / '_config' - monkeypatch.setitem(os.environ, 'XDG_CONFIG_HOME', str(fake_config)) - expected = str(tmpdir / '_config') - assert SpecialResolver(Path).user.config == expected - - def test_unix_paths_fallback(self, tmpdir, monkeypatch, feign_linux): - "Without XDG_CONFIG_HOME set, ~/.config should be used." - fake_home = tmpdir / '_home' - monkeypatch.delitem(os.environ, 'XDG_CONFIG_HOME', raising=False) - monkeypatch.setitem(os.environ, 'HOME', str(fake_home)) - expected = Path('~/.config').expanduser() - assert SpecialResolver(Path).user.config == expected - - def test_property(self): - assert isinstance(Path.special().user.config, Path) - assert isinstance(Path.special().user.data, Path) - assert isinstance(Path.special().user.cache, Path) - - def test_other_parameters(self): - """ - Other parameters should be passed through to appdirs function. - """ - res = Path.special(version="1.0", multipath=True).site.config - assert isinstance(res, Path) - - def test_multipath(self, feign_linux, monkeypatch, tmpdir): - """ - If multipath is provided, on Linux return the XDG_CONFIG_DIRS - """ - fake_config_1 = str(tmpdir / '_config1') - fake_config_2 = str(tmpdir / '_config2') - config_dirs = os.pathsep.join([fake_config_1, fake_config_2]) - monkeypatch.setitem(os.environ, 'XDG_CONFIG_DIRS', config_dirs) - res = Path.special(multipath=True).site.config - assert isinstance(res, Multi) - assert fake_config_1 in res - assert fake_config_2 in res - assert '_config1' in str(res) - - def test_reused_SpecialResolver(self): - """ - Passing additional args and kwargs to SpecialResolver should be - passed through to each invocation of the function in appdirs. - """ - appdirs = importlib.import_module('appdirs') - - adp = SpecialResolver(Path, version="1.0") - res = adp.user.config - - expected = appdirs.user_config_dir(version="1.0") - assert res == expected - - -class TestMultiPath: - def test_for_class(self): - """ - Multi.for_class should return a subclass of the Path class provided. - """ - cls = Multi.for_class(Path) - assert issubclass(cls, Path) - assert issubclass(cls, Multi) - expected_name = 'Multi' + Path.__name__ - assert cls.__name__ == expected_name - - def test_detect_no_pathsep(self): - """ - If no pathsep is provided, multipath detect should return an instance - of the parent class with no Multi mix-in. - """ - path = Multi.for_class(Path).detect('/foo/bar') - assert isinstance(path, Path) - assert not isinstance(path, Multi) - - def test_detect_with_pathsep(self): - """ - If a pathsep appears in the input, detect should return an instance - of a Path with the Multi mix-in. - """ - inputs = '/foo/bar', '/baz/bing' - input = os.pathsep.join(inputs) - path = Multi.for_class(Path).detect(input) - - assert isinstance(path, Multi) - - def test_iteration(self): - """ - Iterating over a MultiPath should yield instances of the - parent class. - """ - inputs = '/foo/bar', '/baz/bing' - input = os.pathsep.join(inputs) - path = Multi.for_class(Path).detect(input) - - items = iter(path) - first = next(items) - assert first == '/foo/bar' - assert isinstance(first, Path) - assert not isinstance(first, Multi) - assert next(items) == '/baz/bing' - assert path == input - - -def test_no_dependencies(): - """ - Path.py guarantees that the path module can be - transplanted into an environment without any dependencies. - """ - cmd = [sys.executable, '-S', '-c', 'import path'] - subprocess.check_call(cmd) - - -def test_version(): - """ - Under normal circumstances, path should present a - __version__. - """ - assert re.match(r'\d+\.\d+.*', path.__version__) From 5564d7c0c7a7831959d28e0c1af95869b26bd3ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Nov 2019 18:19:54 -0500 Subject: [PATCH 301/835] Update changelog and use minor version bump. --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bbe50ba7..89fc6c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ -v12.2.1 +v12.3.0 ------- -- #169: Project is renamed to simply ``path``. This release is - the last planned release under the name ``path.py``. +- #169: Project is renamed to simply ``path``. This release of + ``path.py`` simply depends on ``path < 13.1``. v12.2.0 ------- From b10e8186a9305d1f899f0fd7d1319b7b15ed1ecd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 20:33:33 -0500 Subject: [PATCH 302/835] Rebrand to 'For Enterprise' --- docs/_templates/tidelift-sidebar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html index c89c0f09..ce48f46b 100644 --- a/docs/_templates/tidelift-sidebar.html +++ b/docs/_templates/tidelift-sidebar.html @@ -1,6 +1,6 @@ -

Professional support

+

For Enterprise

Professionally-supported {{ project }} is available with the -Tidelift Subscription. +Tidelift Subscription.

From bb357fb88ca6df10b84235eff42bafa1461b2b75 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 20:38:21 -0500 Subject: [PATCH 303/835] Add a 'For Enterprise' section to the README --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 420bfb4f..7b317c71 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,15 @@ .. image:: https://tidelift.com/badges/package/pypi/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. + Security Contact ================ From 9ef4b6e60389a8f39cc04e466d12f42861c26472 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 21:08:31 -0500 Subject: [PATCH 304/835] Prefer 'path' to 'path.py' --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 889af7a4..fa2a72f4 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ skip_install = True deps = pep517>=0.5 twine[keyring]>=1.13 - path.py + path passenv = TWINE_PASSWORD setenv = From 5c93ac90dace92cb2815d16d7ee87cfa8a4b6531 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 12:24:31 -0500 Subject: [PATCH 305/835] Add 'read_text' and 'read_bytes' methods. Fixes #170. --- path/__init__.py | 22 +++++++++++++++++----- test_path.py | 11 ++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 240973f4..662b3430 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -703,16 +703,28 @@ def write_bytes(self, bytes, append=False): with self.open(mode) as f: f.write(bytes) - def text(self, encoding=None, errors='strict'): + def read_text(self, encoding=None, errors=None): r""" Open this file, read it in, return the content as a string. - All newline sequences are converted to ``'\n'``. Keyword arguments - will be passed to :meth:`open`. + Optional parameters are passed to :meth:`open`. .. seealso:: :meth:`lines` """ - with self.open(mode='r', encoding=encoding, errors=errors) as f: - return U_NEWLINE.sub('\n', f.read()) + with self.open(encoding=encoding, errors=errors) as f: + return f.read() + + def read_bytes(self): + r"""Return the contents of this file as bytes.""" + with self.open(mode='rb') as f: + return f.read() + + def text(self, encoding=None, errors='strict'): + """Legacy function to read text. + + Converts all newline sequences to ``\n``. + """ + warnings.warn(".text is deprecated; use read_text", DeprecationWarning) + return U_NEWLINE.sub('\n', self.read_text(encoding, errors)) def write_text( self, text, encoding=None, errors='strict', linesep=os.linesep, append=False diff --git a/test_path.py b/test_path.py index 10fce8fe..f192df8c 100644 --- a/test_path.py +++ b/test_path.py @@ -225,6 +225,14 @@ def test_joinpath_returns_same_type(self): assert res2 == 'foo/bar' +class TestReadWriteText: + def test_read_write(self, tmpdir): + file = path.Path(tmpdir) / 'filename' + file.write_text('hello world') + assert file.read_text() == 'hello world' + assert file.read_bytes() == b'hello world' + + class TestPerformance: @staticmethod def get_command_time(cmd): @@ -570,6 +578,7 @@ def test_patterns(self, tmpdir): self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) + @pytest.mark.filterwarnings("ignore:.text is deprecated") def test_unicode(self, tmpdir): d = Path(tmpdir) p = d / 'unicode.txt' @@ -862,7 +871,7 @@ def test_only_newer(self): self.subdir_a.merge_tree( self.subdir_b, copy_function=path.only_newer(shutil.copy2) ) - assert target.text() == 'this is newer' + assert target.read_text() == 'this is newer' class TestChdir: From 6dbd8b0bdeece460b55d320bc32b60d3aa650cf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 12:37:44 -0500 Subject: [PATCH 306/835] Update changelog. Ref #170. --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 70f4fd6a..fa9d3d7b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +v13.1.0 +------- + +- #170: Added ``read_text`` and ``read_bytes`` methods to + align with ``pathlib`` behavior. Deprecated ``text`` method. + If you require newline normalization of ``text``, use + ``jaraco.text.normalize_newlines(Path.read_text())``. + v13.0.0 ------- From 13684846bb5dacfc361c9693276d35b1f086e305 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 12:47:33 -0500 Subject: [PATCH 307/835] Extract encodings as a test parameter. --- test_path.py | 246 +++++++++++++++++++++++++-------------------------- 1 file changed, 120 insertions(+), 126 deletions(-) diff --git a/test_path.py b/test_path.py index f192df8c..1b1a14ae 100644 --- a/test_path.py +++ b/test_path.py @@ -579,135 +579,129 @@ def test_patterns(self, tmpdir): self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) @pytest.mark.filterwarnings("ignore:.text is deprecated") - def test_unicode(self, tmpdir): + @pytest.mark.parametrize("encoding", ('UTF-8', 'UTF-16BE', 'UTF-16LE', 'UTF-16')) + def test_unicode(self, tmpdir, encoding): + """ Test that path works with the specified encoding, + which must be capable of representing the entire range of + Unicode codepoints. + """ d = Path(tmpdir) p = d / 'unicode.txt' - def test(enc): - """ Test that path works with the specified encoding, - which must be capable of representing the entire range of - Unicode codepoints. - """ - - given = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\r\n' - '\u0d0a\u0a0d\u0d15\u0a15\x85' - '\u0d0a\u0a0d\u0d15\u0a15\u2028' - '\r' - 'hanging' - ) - clean = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\n' - 'hanging' - ) - givenLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\r\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\x85'), - ('\u0d0a\u0a0d\u0d15\u0a15\u2028'), - ('\r'), - ('hanging'), - ] - expectedLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\n'), - ('hanging'), - ] - expectedLines2 = [ - ('Hello world'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - (''), - ('hanging'), - ] - - # write bytes manually to file - f = codecs.open(p, 'w', enc) - f.write(given) - f.close() - - # test all 3 path read-fully functions, including - # path.lines() in unicode mode. - assert p.bytes() == given.encode(enc) - assert p.text(enc) == clean - assert p.lines(enc) == expectedLines - assert p.lines(enc, retain=False) == expectedLines2 - - # If this is UTF-16, that's enough. - # The rest of these will unfortunately fail because append=True - # mode causes an extra BOM to be written in the middle of the file. - # UTF-16 is the only encoding that has this problem. - if enc == 'UTF-16': - return - - # Write Unicode to file using path.write_text(). - # This test doesn't work with a hanging line. - cleanNoHanging = clean + '\n' - - p.write_text(cleanNoHanging, enc) - p.write_text(cleanNoHanging, enc, append=True) - # Check the result. - expectedBytes = 2 * cleanNoHanging.replace('\n', os.linesep).encode(enc) - expectedLinesNoHanging = expectedLines[:] - expectedLinesNoHanging[-1] += '\n' - assert p.bytes() == expectedBytes - assert p.text(enc) == 2 * cleanNoHanging - assert p.lines(enc) == 2 * expectedLinesNoHanging - assert p.lines(enc, retain=False) == 2 * expectedLines2 - - # Write Unicode to file using path.write_lines(). - # The output in the file should be exactly the same as last time. - p.write_lines(expectedLines, enc) - p.write_lines(expectedLines2, enc, append=True) - # Check the result. - assert p.bytes() == expectedBytes - - # Now: same test, but using various newline sequences. - # If linesep is being properly applied, these will be converted - # to the platform standard newline sequence. - p.write_lines(givenLines, enc) - p.write_lines(givenLines, enc, append=True) - # Check the result. - assert p.bytes() == expectedBytes - - # Same test, using newline sequences that are different - # from the platform default. - def testLinesep(eol): - p.write_lines(givenLines, enc, linesep=eol) - p.write_lines(givenLines, enc, linesep=eol, append=True) - expected = 2 * cleanNoHanging.replace('\n', eol).encode(enc) - assert p.bytes() == expected - - testLinesep('\n') - testLinesep('\r') - testLinesep('\r\n') - testLinesep('\x0d\x85') - - # Again, but with linesep=None. - p.write_lines(givenLines, enc, linesep=None) - p.write_lines(givenLines, enc, linesep=None, append=True) - # Check the result. - expectedBytes = 2 * given.encode(enc) - assert p.bytes() == expectedBytes - assert p.text(enc) == 2 * clean - expectedResultLines = expectedLines[:] - expectedResultLines[-1] += expectedLines[0] - expectedResultLines += expectedLines[1:] - assert p.lines(enc) == expectedResultLines - - test('UTF-8') - test('UTF-16BE') - test('UTF-16LE') - test('UTF-16') + given = ( + 'Hello world\n' + '\u0d0a\u0a0d\u0d15\u0a15\r\n' + '\u0d0a\u0a0d\u0d15\u0a15\x85' + '\u0d0a\u0a0d\u0d15\u0a15\u2028' + '\r' + 'hanging' + ) + clean = ( + 'Hello world\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\n' + 'hanging' + ) + givenLines = [ + ('Hello world\n'), + ('\u0d0a\u0a0d\u0d15\u0a15\r\n'), + ('\u0d0a\u0a0d\u0d15\u0a15\x85'), + ('\u0d0a\u0a0d\u0d15\u0a15\u2028'), + ('\r'), + ('hanging'), + ] + expectedLines = [ + ('Hello world\n'), + ('\u0d0a\u0a0d\u0d15\u0a15\n'), + ('\u0d0a\u0a0d\u0d15\u0a15\n'), + ('\u0d0a\u0a0d\u0d15\u0a15\n'), + ('\n'), + ('hanging'), + ] + expectedLines2 = [ + ('Hello world'), + ('\u0d0a\u0a0d\u0d15\u0a15'), + ('\u0d0a\u0a0d\u0d15\u0a15'), + ('\u0d0a\u0a0d\u0d15\u0a15'), + (''), + ('hanging'), + ] + + # write bytes manually to file + f = codecs.open(p, 'w', encoding) + f.write(given) + f.close() + + # test all 3 path read-fully functions, including + # path.lines() in unicode mode. + assert p.bytes() == given.encode(encoding) + assert p.text(encoding) == clean + assert p.lines(encoding) == expectedLines + assert p.lines(encoding, retain=False) == expectedLines2 + + # If this is UTF-16, that's enough. + # The rest of these will unfortunately fail because append=True + # mode causes an extra BOM to be written in the middle of the file. + # UTF-16 is the only encoding that has this problem. + if encoding == 'UTF-16': + return + + # Write Unicode to file using path.write_text(). + # This test doesn't work with a hanging line. + cleanNoHanging = clean + '\n' + + p.write_text(cleanNoHanging, encoding) + p.write_text(cleanNoHanging, encoding, append=True) + # Check the result. + expectedBytes = 2 * cleanNoHanging.replace('\n', os.linesep).encode(encoding) + expectedLinesNoHanging = expectedLines[:] + expectedLinesNoHanging[-1] += '\n' + assert p.bytes() == expectedBytes + assert p.text(encoding) == 2 * cleanNoHanging + assert p.lines(encoding) == 2 * expectedLinesNoHanging + assert p.lines(encoding, retain=False) == 2 * expectedLines2 + + # Write Unicode to file using path.write_lines(). + # The output in the file should be exactly the same as last time. + p.write_lines(expectedLines, encoding) + p.write_lines(expectedLines2, encoding, append=True) + # Check the result. + assert p.bytes() == expectedBytes + + # Now: same test, but using various newline sequences. + # If linesep is being properly applied, these will be converted + # to the platform standard newline sequence. + p.write_lines(givenLines, encoding) + p.write_lines(givenLines, encoding, append=True) + # Check the result. + assert p.bytes() == expectedBytes + + # Same test, using newline sequences that are different + # from the platform default. + def testLinesep(eol): + p.write_lines(givenLines, encoding, linesep=eol) + p.write_lines(givenLines, encoding, linesep=eol, append=True) + expected = 2 * cleanNoHanging.replace('\n', eol).encode(encoding) + assert p.bytes() == expected + + testLinesep('\n') + testLinesep('\r') + testLinesep('\r\n') + testLinesep('\x0d\x85') + + # Again, but with linesep=None. + p.write_lines(givenLines, encoding, linesep=None) + p.write_lines(givenLines, encoding, linesep=None, append=True) + # Check the result. + expectedBytes = 2 * given.encode(encoding) + assert p.bytes() == expectedBytes + assert p.text(encoding) == 2 * clean + expectedResultLines = expectedLines[:] + expectedResultLines[-1] += expectedLines[0] + expectedResultLines += expectedLines[1:] + assert p.lines(encoding) == expectedResultLines def test_chunks(self, tmpdir): p = (TempDir() / 'test.txt').touch() From 9a3f1cc315297f5540e560093ca7636aead4b2f3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 12:51:42 -0500 Subject: [PATCH 308/835] Remove multi-Path-class fixture. --- test_path.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/test_path.py b/test_path.py index 1b1a14ae..b4a22323 100644 --- a/test_path.py +++ b/test_path.py @@ -33,27 +33,18 @@ import packaging.version import path +from path import Path from path import TempDir from path import matchers from path import SpecialResolver from path import Multi -Path = None - def p(**choices): """ Choose a value from several possible values, based on os.name """ return choices[os.name] -@pytest.fixture(autouse=True, params=[path.Path]) -def path_class(request, monkeypatch): - """ - Invoke tests on any number of Path classes. - """ - monkeypatch.setitem(globals(), 'Path', request.param) - - def mac_version(target, comparator=operator.ge): """ Return True if on a Mac whose version passes the comparator. From a7763136702cb8bf2165ee2a33394274d32ab799 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 12:55:30 -0500 Subject: [PATCH 309/835] Limit test to run only where expected to be viable. --- test_path.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_path.py b/test_path.py index b4a22323..b1c2ba6e 100644 --- a/test_path.py +++ b/test_path.py @@ -402,9 +402,9 @@ def test_listing(self, tmpdir): except Exception: pass - @pytest.mark.xfail(mac_version('10.13'), reason="macOS disallows invalid encodings") - @pytest.mark.xfail( - platform.system() == 'Windows', reason="Can't write latin characters. See #133" + @pytest.mark.skip( + platform.system() != "Linux", + reason="Only Linux allows writing invalid encodings", ) def test_listdir_other_encoding(self, tmpdir): """ From d0a2f6f0f4e56b588316cc92add2aa4fd4e26bae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 13:04:45 -0500 Subject: [PATCH 310/835] Bump path dependency --- CHANGES.rst | 5 +++++ setup.cfg | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 89fc6c34..c5702521 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v12.4.0 +------- + +- #169: Project now depends on ``path < 13.2``. + v12.3.0 ------- diff --git a/setup.cfg b/setup.cfg index 17ab190c..38e52094 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = find: include_package_data = true python_requires = >=3.5 install_requires = - path < 13.1 + path < 13.2 setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] From d95b2db65ea98bd6a89e7db079b4518d1c68f616 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 13:05:33 -0500 Subject: [PATCH 311/835] Fix link in readme --- CHANGES.rst | 1 + README.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c5702521..24c78916 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,7 @@ v12.4.0 ------- - #169: Project now depends on ``path < 13.2``. +- Fixed typo in README. v12.3.0 ------- diff --git a/README.rst b/README.rst index 4a46c57d..4bc12148 100644 --- a/README.rst +++ b/README.rst @@ -1 +1 @@ -``path.py`` has been renamed to `path `. +``path.py`` has been renamed to `path `_. From 266a9c112ce2fb38365a13f57122fa73754555ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 8 Dec 2019 12:25:04 -0500 Subject: [PATCH 312/835] Cover Python 3.8 in Windows tests --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index bfd57529..6a1c99a9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ environment: matrix: - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38-x64" install: # symlink python from a directory with a space From 6454232a9367e503152ffb9304a64ed9379c526d Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Tue, 17 Dec 2019 17:10:31 +1100 Subject: [PATCH 313/835] Fix simple typo: temporry -> temporary Closes #180 --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index b1c2ba6e..5fc6bf1a 100644 --- a/test_path.py +++ b/test_path.py @@ -949,7 +949,7 @@ def test_context_manager_exception(self): def test_context_manager_using_with(self): """ The context manager will allow using the with keyword and - provide a temporry directory that will be deleted after that. + provide a temporary directory that will be deleted after that. """ with TempDir() as d: From 76786bdd11eac589bdaeb9a8cea5dcacaf613225 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 20 Dec 2019 23:49:53 -0500 Subject: [PATCH 314/835] Update black in pre-commit and add blacken-docs. --- .pre-commit-config.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e16c59ac..fe46b8c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black + +- repo: https://github.com/asottile/blacken-docs + rev: v1.4.0 + hooks: + - id: blacken-docs From 051ce14211226aa34e11dc963cef508dd8ccdc53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jan 2020 16:39:37 -0500 Subject: [PATCH 315/835] Test and release using Azure Pipelines --- .travis.yml | 9 ------ README.rst | 3 ++ azure-pipelines.yml | 71 +++++++++++++++++++++++++++++++++++++++++++++ skeleton.md | 34 ++++++++++++++++++---- 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 azure-pipelines.yml diff --git a/.travis.yml b/.travis.yml index 45cbcf94..17e45a67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,15 +5,6 @@ python: - 3.6 - &latest_py3 3.8 -jobs: - fast_finish: true - include: - - stage: deploy - if: tag IS present - python: *latest_py3 - before_script: skip - script: tox -e release - cache: pip install: diff --git a/README.rst b/README.rst index 50eba567..a234ec9b 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,9 @@ .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg +.. image:: https://dev.azure.com/jaraco/skeleton/_apis/build/status/jaraco.skeleton?branchName=master + :target: https://dev.azure.com/jaraco/skeleton/_build/latest?definitionId=1&branchName=master + .. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg :target: https://travis-ci.org/jaraco/skeleton diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..d461bd00 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,71 @@ +# Create the project in Azure with: +# az project create --name $name --organization https://dev.azure.com/$org/ --visibility public +# then configure the pipelines (through web UI) + +trigger: + branches: + include: + - '*' + tags: + include: + - '*' + +pool: + vmimage: 'Ubuntu-18.04' + +variables: +- group: Azure secrets + +stages: +- stage: Test + jobs: + + - job: 'Test' + strategy: + matrix: + Python36: + python.version: '3.6' + Python38: + python.version: '3.8' + maxParallel: 4 + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + + - script: python -m pip install tox + displayName: 'Install tox' + + - script: | + tox -- --junit-xml=test-results.xml + displayName: 'run tests' + + - task: PublishTestResults@2 + inputs: + testResultsFiles: '**/test-results.xml' + testRunTitle: 'Python $(python.version)' + condition: succeededOrFailed() + +- stage: Publish + dependsOn: Test + jobs: + - job: 'Publish' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.8' + architecture: 'x64' + + - script: python -m pip install tox + displayName: 'Install tox' + + - script: | + tox -e release + env: + TWINE_PASSWORD: $(PyPI-token) + displayName: 'publish to PyPI' + + condition: contains(variables['Build.SourceBranch'], 'tags') diff --git a/skeleton.md b/skeleton.md index ee26e14b..7e9955c9 100644 --- a/skeleton.md +++ b/skeleton.md @@ -103,23 +103,47 @@ Relies a .flake8 file to correct some default behaviors: ## Continuous Integration -The project is pre-configured to run tests in [Travis-CI](https://travis-ci.org) (.travis.yml). Any new project must be enabled either through their web site or with the `travis enable` command. +The project is pre-configured to run tests through multiple CI providers. + +### Azure Pipelines + +[Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) are the preferred provider as they provide free, fast, multi-platform services. See azure-pipelines.yml for more details. + +Features include: + +- test against multiple Python versions +- run on Ubuntu Bionic + +### Travis CI + +[Travis-CI](https://travis-ci.org) is configured through .travis.yml. Any new project must be enabled either through their web site or with the `travis enable` command. Features include: -- test against Python 2 and 3 +- test against 3 - run on Ubuntu Xenial - correct for broken IPv6 -Also provided is a minimal template for running under Appveyor (Windows). +### Appveyor + +A minimal template for running under Appveyor (Windows) is provided. ### Continuous Deployments -In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Travis as the TWINE_PASSWORD environment variable. After the Travis project is created, configure the token through the web UI or with a command like the following (bash syntax): +In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Azure as the `Azure secrets` variable group. This variable group needs to be created only once per organization. For example: ``` -TWINE_PASSWORD={token} travis env copy TWINE_PASSWORD +# create a resource group if none exists +az group create --name main --location eastus2 +# create the vault (try different names until something works) +az keyvault create --name secrets007 --resource-group main +# create the secret +az keyvault secret set --vault-name secrets007 --name PyPI-token --value $token ``` +Then, in the web UI for the project's Pipelines Library, create the `Azure secrets` variable group referencing the key vault name. + +For more details, see [this blog entry](https://blog.jaraco.com/configuring-azure-pipelines-with-secets/). + ## Building Documentation Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. From b07b27332dcefc9ae9ad0a4d35ca4f39fa358233 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Jan 2020 17:18:12 -0500 Subject: [PATCH 316/835] Correct guidance on project creation. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d461bd00..3e80bf44 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,5 @@ # Create the project in Azure with: -# az project create --name $name --organization https://dev.azure.com/$org/ --visibility public +# az devops project create --name $name --organization https://dev.azure.com/$org/ --visibility public # then configure the pipelines (through web UI) trigger: From 34d87688eae5d10970f3c8269ac01bcca4ad0229 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Jan 2020 17:23:00 -0500 Subject: [PATCH 317/835] Include token passthrough for azure pipelines publish stage. --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..01bfa5f5 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,2 @@ + env: + TIDELIFT_TOKEN: $(Tidelift-token) From 0bf3d43d97d5466fddac708e7ca38ba95281c6e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Jan 2020 13:36:43 -0500 Subject: [PATCH 318/835] Rely on setuptools_scm 3.4 and setuptools 42. Now setup.py is optional. Remove setuptools from test environment. --- pyproject.toml | 4 +++- setup.cfg | 2 +- setup.py | 2 +- tox.ini | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3afc8c33..74cff744 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,8 @@ [build-system] -requires = ["setuptools>=34.4", "wheel", "setuptools_scm>=1.15"] +requires = ["setuptools>=42", "wheel", "setuptools_scm>=3.4"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true + +[tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg index 2c0f7b1f..bc7a9d5d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ packages = find: include_package_data = true python_requires = >=3.6 install_requires = -setup_requires = setuptools_scm >= 1.15.0 +setup_requires = setuptools_scm >= 3.4 [options.extras_require] testing = diff --git a/setup.py b/setup.py index 827e955f..bac24a43 100644 --- a/setup.py +++ b/setup.py @@ -3,4 +3,4 @@ import setuptools if __name__ == "__main__": - setuptools.setup(use_scm_version=True) + setuptools.setup() diff --git a/tox.ini b/tox.ini index fa2a72f4..fb98930e 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,6 @@ requires = [testenv] deps = - setuptools>=31.0.1 pip_version = pip commands = pytest {posargs} From d9934dc8f5dafb93ca565eb0f8ec9e0c245b8d68 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 18 Jan 2020 20:50:32 +0200 Subject: [PATCH 319/835] Spelling and capitalisation (#8) Co-authored-by: Jason R. Coombs --- skeleton.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/skeleton.md b/skeleton.md index 7e9955c9..439df2f0 100644 --- a/skeleton.md +++ b/skeleton.md @@ -2,9 +2,9 @@ This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. -## An SCM Managed Approach +## An SCM-Managed Approach -While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a git repo capturing the evolution and culmination of these best practices. +While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a Git repo capturing the evolution and culmination of these best practices. It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. @@ -38,7 +38,7 @@ The `--allow-unrelated-histories` is necessary because the history from the skel ## Updating -Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar git operations. +Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar Git operations. Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. @@ -46,16 +46,16 @@ Thereafter, the target project can make whatever customizations it deems relevan The features/techniques employed by the skeleton include: -- PEP 517/518 based build relying on setuptools as the build tool -- setuptools declarative configuration using setup.cfg +- PEP 517/518-based build relying on Setuptools as the build tool +- Setuptools declarative configuration using setup.cfg - tox for running tests -- A README.rst as reStructuredText with some popular badges, but with readthedocs and appveyor badges commented out +- A README.rst as reStructuredText with some popular badges, but with Read the Docs and AppVeyor badges commented out - A CHANGES.rst file intended for publishing release notes about the project -- Use of [black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) +- Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) ## Packaging Conventions -A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on setuptools (a minimum version compatible with setup.cfg declarative config). +A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on Setuptools (a minimum version compatible with setup.cfg declarative config). The setup.cfg file implements the following features: @@ -92,14 +92,14 @@ A pytest.ini is included to define common options around running tests. In parti - rely on default test discovery in the current directory - avoid recursing into common directories not containing tests -- run doctests on modules and invoke flake8 tests -- in doctests, allow unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. +- run doctests on modules and invoke Flake8 tests +- in doctests, allow Unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. - filters out known warnings caused by libraries/functionality included by the skeleton -Relies a .flake8 file to correct some default behaviors: +Relies on a .flake8 file to correct some default behaviors: - disable mutually incompatible rules W503 and W504 -- support for black format +- support for Black format ## Continuous Integration @@ -116,10 +116,10 @@ Features include: ### Travis CI -[Travis-CI](https://travis-ci.org) is configured through .travis.yml. Any new project must be enabled either through their web site or with the `travis enable` command. +[Travis CI](https://travis-ci.org) is configured through .travis.yml. Any new project must be enabled either through their web site or with the `travis enable` command. Features include: -- test against 3 +- test against Python 3 - run on Ubuntu Xenial - correct for broken IPv6 @@ -148,7 +148,7 @@ For more details, see [this blog entry](https://blog.jaraco.com/configuring-azur Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. -In addition to building the sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. +In addition to building the Sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. ## Cutting releases From 7558cfe2eb2f1ffe3676905e9871466cbc9da24f Mon Sep 17 00:00:00 2001 From: johnthagen Date: Tue, 14 Jan 2020 07:53:19 -0500 Subject: [PATCH 320/835] Line wrap LICENSE file --- LICENSE | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 5e795a61..353924be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,19 @@ Copyright Jason R. Coombs -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. From 479ac2149d872757160732bc977977ee0192ac51 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Jan 2020 10:51:33 -0500 Subject: [PATCH 321/835] Finish dropping support for Python 2 (I hope). --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index bc7a9d5d..3e621072 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE name = skeleton @@ -14,6 +11,7 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only [options] packages = find: From 29c497286e9f8e835c68b7b4ddb9ff3635a273dc Mon Sep 17 00:00:00 2001 From: layday <31134424+layday@users.noreply.github.com> Date: Thu, 23 Jan 2020 03:48:38 +0200 Subject: [PATCH 322/835] Require toml extra for setuptools_scm (#12) * Require toml extra for setuptools_scm setuptools_scm does not know to invoke itself if it can't read pyproject.toml. This broke sdist installs for projects deriving from skeleton: $ python -m pip install zipp --no-binary zipp Collecting zipp [...] Successfully installed zipp-0.0.0 Note the version number defaulting to '0.0.0'. Building locally only works because pep517, the build tool, depends on toml which it exposes to the build environment. * Require setuptools_scm 3.4.1 at a minimum A bare [tool.setuptools_scm] does not work in 3.4.0. * fixup! Require toml extra for setuptools_scm * fixup! Require setuptools_scm 3.4.1 at a minimum --- pyproject.toml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74cff744..6ee7df23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm>=3.4"] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] diff --git a/setup.cfg b/setup.cfg index 3e621072..c20fa103 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ packages = find: include_package_data = true python_requires = >=3.6 install_requires = -setup_requires = setuptools_scm >= 3.4 +setup_requires = setuptools_scm[toml] >= 3.4.1 [options.extras_require] testing = From 9a7d846ae8775e5c49cc9933fa897cc761c3b20b Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 18 Jan 2020 20:53:39 +0200 Subject: [PATCH 323/835] Fix AppVeyor typo --- skeleton.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skeleton.md b/skeleton.md index 439df2f0..340b65cf 100644 --- a/skeleton.md +++ b/skeleton.md @@ -123,9 +123,9 @@ Features include: - run on Ubuntu Xenial - correct for broken IPv6 -### Appveyor +### AppVeyor -A minimal template for running under Appveyor (Windows) is provided. +A minimal template for running under AppVeyor (Windows) is provided. ### Continuous Deployments From 0aa6c5c191ad22bbc4ad30d0880e531d4e66c0b3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 23 Jan 2020 22:51:29 +0200 Subject: [PATCH 324/835] Link badge to PyPI rather than static image And DRY the link --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a234ec9b..4c7fd554 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,10 @@ .. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: https://pypi.org/project/skeleton + :target: `PyPI link`_ .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/skeleton .. image:: https://dev.azure.com/jaraco/skeleton/_apis/build/status/jaraco.skeleton?branchName=master :target: https://dev.azure.com/jaraco/skeleton/_build/latest?definitionId=1&branchName=master From cf989c8eab6b9cf08ba644b9b7e3b8c4b06a52fc Mon Sep 17 00:00:00 2001 From: J Alan Brogan Date: Mon, 27 Jan 2020 08:12:03 +0000 Subject: [PATCH 325/835] Readability counts --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 72a05212..121ba6da 100644 --- a/README.rst +++ b/README.rst @@ -127,6 +127,11 @@ Releasing Tagged releases are automatically published to PyPI by Travis-CI, assuming the tests pass. +History +======= + +Based on code by Jason Orendorff + Security Contact ================ From 0b681b040f3e13793c61c1dfe6e510d9e3a8870a Mon Sep 17 00:00:00 2001 From: Vincent Fazio Date: Tue, 4 Feb 2020 14:51:03 -0600 Subject: [PATCH 326/835] setup.cfg: let python-tag mirror python_requires In order to generate a wheel in accordance with PEP 425 to restrict the minimum required version of Python (3.6), the `python-tag` bdist_wheel option needs to be specified so the wheel gets tagged properly. Before: zipp-x.x.x-py3-none-any.whl After: zipp-x.x.x-py36-none-any.whl Signed-off-by: Vincent Fazio --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index c20fa103..f2643ea2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,9 @@ classifiers = Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only +[bdist_wheel] +python-tag=py36 + [options] packages = find: include_package_data = true From eb00cd0636bc3e4cef9217b2c5eccdcaebbe5d65 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Feb 2020 09:29:18 -0500 Subject: [PATCH 327/835] Normalize whitespace --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f2643ea2..3436e6b0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ classifiers = Programming Language :: Python :: 3 :: Only [bdist_wheel] -python-tag=py36 +python-tag = py36 [options] packages = find: From 4f845452dd0f7e95b7959b8e0ea50374c73b7920 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Feb 2020 11:23:25 -0500 Subject: [PATCH 328/835] Revert "setup.cfg: let python-tag mirror python_requires" This reverts commit 0b681b040f3e13793c61c1dfe6e510d9e3a8870a. Ref jaraco/zipp#42 --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3436e6b0..c20fa103 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,9 +13,6 @@ classifiers = Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only -[bdist_wheel] -python-tag = py36 - [options] packages = find: include_package_data = true From 879bde503f63e6a151427c6b4df9d205ac784f9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Feb 2020 20:49:43 -0500 Subject: [PATCH 329/835] Fade to black --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index b1c2ba6e..ea047fc4 100644 --- a/test_path.py +++ b/test_path.py @@ -425,7 +425,7 @@ def test_listdir_other_encoding(self, tmpdir): # now try with path results = Path(tmpdir).listdir() assert len(results) == 1 - res, = results + (res,) = results assert isinstance(res, Path) # OS X seems to encode the bytes in the filename as %XX characters. if platform.system() == 'Darwin': From 8a1865b4b66126ca2cd73732a21389d57c40a16e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Feb 2020 20:59:42 -0500 Subject: [PATCH 330/835] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fa9d3d7b..4d5fa24c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v13.2.0 +------- + +- Require Python 3.6 or later. + v13.1.0 ------- From b184254df11705a1937eb7a5dfffd35aad28d7f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Feb 2020 21:28:28 -0500 Subject: [PATCH 331/835] Update changelog. --- README.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index a64ddb0c..ab3d583d 100644 --- a/README.rst +++ b/README.rst @@ -131,13 +131,15 @@ for links to the CI runs. Releasing ========= -Tagged releases are automatically published to PyPI by Travis-CI, assuming -the tests pass. +Tagged releases are automatically published to PyPI by Azure +Pipelines, assuming the tests pass. -History +Origins ======= -Based on code by Jason Orendorff +The ``path.py`` project was initially released in 2003 by Jason Orendorff +and has been continuously developed and supported by several maintainers +over the years. Security Contact ================ From 8fbd841d17a029232577d71587c02aa429874287 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Feb 2020 17:36:22 -0600 Subject: [PATCH 332/835] Update to bionic for Travis. Correct comment about IPv6 workaround. --- .travis.yml | 4 ++-- skeleton.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17e45a67..923377f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic language: python python: @@ -11,7 +11,7 @@ install: - pip install tox tox-venv before_script: - # Disable IPv6. Ref travis-ci/travis-ci#8361 + # Enable IPv6. Ref travis-ci/travis-ci#8361 - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi diff --git a/skeleton.md b/skeleton.md index 340b65cf..4544158f 100644 --- a/skeleton.md +++ b/skeleton.md @@ -120,7 +120,7 @@ Features include: Features include: - test against Python 3 -- run on Ubuntu Xenial +- run on Ubuntu Bionic - correct for broken IPv6 ### AppVeyor From b4dd44cc7c8da44bcc8f2674d2a5d764091f63c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Mar 2020 15:05:37 -0400 Subject: [PATCH 333/835] Suppress warnings in pytest-flake8, pytest-black, and pytest-checkdocs. --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 7b9b714f..60d9bf76 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,5 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= + # https://github.com/pytest-dev/pytest/issues/6928 + ignore:direct construction of .*Item has been deprecated:DeprecationWarning From 2c2fee097e1898a8c00b4434f74063302aeb96e9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Mar 2020 09:22:01 -0400 Subject: [PATCH 334/835] Prefer pytest-black to pytest-black-multipy --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index c20fa103..3f887a64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ testing = pytest >= 3.5, !=3.7.3 pytest-checkdocs >= 1.2.3 pytest-flake8 - pytest-black-multipy + pytest-black >= 0.3.7 pytest-cov # local From 045e6ad88af135f85365248812cbe5ddb3faf355 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Apr 2020 14:51:43 -0400 Subject: [PATCH 335/835] Test against Windows and Mac --- azure-pipelines.yml | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..25e638b5 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,80 @@ +# Create the project in Azure with: +# az devops project create --name $name --organization https://dev.azure.com/$org/ --visibility public +# then configure the pipelines (through web UI) + +trigger: + branches: + include: + - '*' + tags: + include: + - '*' + +pool: + vmImage: $(pool_vm_image) + +variables: +- group: Azure secrets + +stages: +- stage: Test + jobs: + + - job: 'Test' + strategy: + matrix: + Bionic Python 3.6: + python.version: '3.6' + pool_vm_image: Ubuntu-18.04 + Bionic Python 3.8: + python.version: '3.8' + pool_vm_image: Ubuntu-18.04 + Windows: + python.version: '3.8' + pool_vm_image: vs2017-win2016 + MacOS: + python.version: '3.8' + pool_vm_image: macos-10.15 + + maxParallel: 4 + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + + - script: python -m pip install tox + displayName: 'Install tox' + + - script: | + tox -- --junit-xml=test-results.xml + displayName: 'run tests' + + - task: PublishTestResults@2 + inputs: + testResultsFiles: '**/test-results.xml' + testRunTitle: 'Python $(python.version)' + condition: succeededOrFailed() + +- stage: Publish + dependsOn: Test + jobs: + - job: 'Publish' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.8' + architecture: 'x64' + + - script: python -m pip install tox + displayName: 'Install tox' + + - script: | + tox -e release + env: + TWINE_PASSWORD: $(PyPI-token) + displayName: 'publish to PyPI' + + condition: contains(variables['Build.SourceBranch'], 'tags') From bb96dc3fcd738e3202423a0ed09ea9f5dd4ce50b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Apr 2020 09:54:59 -0400 Subject: [PATCH 336/835] Define a default pool_vm_image --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 25e638b5..fd432962 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,6 +15,8 @@ pool: variables: - group: Azure secrets +- name: pool_vm_image + value: Ubuntu-18.04 stages: - stage: Test @@ -25,10 +27,8 @@ stages: matrix: Bionic Python 3.6: python.version: '3.6' - pool_vm_image: Ubuntu-18.04 Bionic Python 3.8: python.version: '3.8' - pool_vm_image: Ubuntu-18.04 Windows: python.version: '3.8' pool_vm_image: vs2017-win2016 From f66d87899937361bea35f437ba293774f0375ca2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Apr 2020 18:49:51 -0400 Subject: [PATCH 337/835] Remove tox-venv and tox-pip-version. Tox-venv is discouraged (https://github.com/tox-dev/tox-venv/issues/48#issuecomment-620227405) and tox-pip-version was only there to support tox-venv. venv is dead; long live virtualenv. --- tox.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tox.ini b/tox.ini index fb98930e..ba5857a3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,15 +3,10 @@ envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true -# Ensure that a late version of pip is used even on tox-venv. -requires = - tox-pip-version>=0.0.6 - tox-venv [testenv] deps = -pip_version = pip commands = pytest {posargs} usedevelop = True From 0df8947fc97f0f0fab6e680d20d8affe5838aec3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 May 2020 23:15:53 -0400 Subject: [PATCH 338/835] Remove more references to tox-venv --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 923377f6..37fa499d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: cache: pip install: -- pip install tox tox-venv +- pip install tox before_script: # Enable IPv6. Ref travis-ci/travis-ci#8361 diff --git a/appveyor.yml b/appveyor.yml index 6a1c99a9..c6f46e4f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ cache: - '%LOCALAPPDATA%\pip\Cache' test_script: - - "python -m pip install -U tox tox-venv virtualenv" + - "python -m pip install -U tox virtualenv" - "tox" version: '{build}' From 649bc7934a13b78eb1283cad9919b9f26c5426e9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 May 2020 07:25:43 -0400 Subject: [PATCH 339/835] Add workaround for warning emitted when junitxml is used. Ref pytest-dev/pytest#6178. --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 60d9bf76..62c0f365 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,8 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules --flake8 --black --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS +# workaround for warning pytest-dev/pytest#6178 +junit_family=xunit2 filterwarnings= # https://github.com/pytest-dev/pytest/issues/6928 ignore:direct construction of .*Item has been deprecated:DeprecationWarning From 7455f2f25310b2d778a648e45d32033ccc790946 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 May 2020 15:31:25 -0400 Subject: [PATCH 340/835] Include mypy for type checking during tests. --- mypy.ini | 2 ++ pytest.ini | 2 +- setup.cfg | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..976ba029 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/pytest.ini b/pytest.ini index 62c0f365..381b3271 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 --black --cov +addopts=--doctest-modules --flake8 --black --cov --mypy doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 diff --git a/setup.cfg b/setup.cfg index 3f887a64..e2dcebb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ testing = pytest-flake8 pytest-black >= 0.3.7 pytest-cov + pytest-mypy # local From 658278cb4d1a106de3281f6d4638ebb4ea1ece90 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 22:41:24 -0400 Subject: [PATCH 341/835] Fix type errors. --- path/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 662b3430..30ca09da 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -69,10 +69,10 @@ try: - from importlib import metadata + from importlib import metadata # type: ignore except ImportError: try: - import importlib_metadata as metadata + import importlib_metadata as metadata # type: ignore except ImportError: pass From 44db9f1825bcb717fe80bc39fccd9e5e68d6faa3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 22:49:30 -0400 Subject: [PATCH 342/835] Suppress more spurious type failures. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 30ca09da..39561ec9 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -77,7 +77,7 @@ pass try: - __version__ = metadata.version('path') + __version__ = metadata.version('path') # type: ignore except Exception: __version__ = 'unknown' From a2065a7e8d9771034835eab5663ab5a27648a0ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 May 2020 23:29:34 -0400 Subject: [PATCH 343/835] Mark test as xfail. Ref #186. --- test_path.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_path.py b/test_path.py index 14448d51..e0b167f9 100644 --- a/test_path.py +++ b/test_path.py @@ -475,6 +475,9 @@ def assertSetsEqual(self, a, b): assert ad == bd + @pytest.mark.xfail( + 'sys.version_info > (3, 8) and platform.system() == "Windows"', reason="#186" + ) def test_shutil(self, tmpdir): # Note: This only tests the methods exist and do roughly what # they should, neglecting the details as they are shutil's From aa3b2eefb8370fdb15da580775758a11c76dc9b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 21:45:57 -0400 Subject: [PATCH 344/835] Remove extraneous parantheses. --- test_path.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test_path.py b/test_path.py index e0b167f9..357eef26 100644 --- a/test_path.py +++ b/test_path.py @@ -599,28 +599,28 @@ def test_unicode(self, tmpdir, encoding): 'hanging' ) givenLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\r\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\x85'), - ('\u0d0a\u0a0d\u0d15\u0a15\u2028'), - ('\r'), - ('hanging'), + 'Hello world\n', + '\u0d0a\u0a0d\u0d15\u0a15\r\n', + '\u0d0a\u0a0d\u0d15\u0a15\x85', + '\u0d0a\u0a0d\u0d15\u0a15\u2028', + '\r', + 'hanging', ] expectedLines = [ - ('Hello world\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\u0d0a\u0a0d\u0d15\u0a15\n'), - ('\n'), - ('hanging'), + 'Hello world\n', + '\u0d0a\u0a0d\u0d15\u0a15\n', + '\u0d0a\u0a0d\u0d15\u0a15\n', + '\u0d0a\u0a0d\u0d15\u0a15\n', + '\n', + 'hanging', ] expectedLines2 = [ - ('Hello world'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - ('\u0d0a\u0a0d\u0d15\u0a15'), - (''), - ('hanging'), + 'Hello world', + '\u0d0a\u0a0d\u0d15\u0a15', + '\u0d0a\u0a0d\u0d15\u0a15', + '\u0d0a\u0a0d\u0d15\u0a15', + '', + 'hanging', ] # write bytes manually to file From 04c993b9f940ece0ee26eef1d37f3cb54ad37021 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 21:49:13 -0400 Subject: [PATCH 345/835] Replace 'expectedLines2' with something explaining its difference from expectedLines. --- test_path.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test_path.py b/test_path.py index 357eef26..28f2496e 100644 --- a/test_path.py +++ b/test_path.py @@ -614,14 +614,7 @@ def test_unicode(self, tmpdir, encoding): '\n', 'hanging', ] - expectedLines2 = [ - 'Hello world', - '\u0d0a\u0a0d\u0d15\u0a15', - '\u0d0a\u0a0d\u0d15\u0a15', - '\u0d0a\u0a0d\u0d15\u0a15', - '', - 'hanging', - ] + expectedLines2 = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file f = codecs.open(p, 'w', encoding) From 31473f5d123d209f3b779e23393accfa6b9297cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 22:18:41 -0400 Subject: [PATCH 346/835] Prefer io.open to codecs.open --- test_path.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test_path.py b/test_path.py index 28f2496e..b9f39841 100644 --- a/test_path.py +++ b/test_path.py @@ -13,7 +13,7 @@ time on files. """ -import codecs +import io import os import sys import shutil @@ -617,9 +617,8 @@ def test_unicode(self, tmpdir, encoding): expectedLines2 = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file - f = codecs.open(p, 'w', encoding) - f.write(given) - f.close() + with io.open(p, 'w', encoding=encoding) as strm: + strm.write(given) # test all 3 path read-fully functions, including # path.lines() in unicode mode. From 8ea90c0d332b30ec70756ab2eab2e0a0f596bda2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 22:22:03 -0400 Subject: [PATCH 347/835] Derive 'given' from 'givenLines' --- test_path.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/test_path.py b/test_path.py index b9f39841..ad75ae70 100644 --- a/test_path.py +++ b/test_path.py @@ -582,22 +582,6 @@ def test_unicode(self, tmpdir, encoding): d = Path(tmpdir) p = d / 'unicode.txt' - given = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\r\n' - '\u0d0a\u0a0d\u0d15\u0a15\x85' - '\u0d0a\u0a0d\u0d15\u0a15\u2028' - '\r' - 'hanging' - ) - clean = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\n' - 'hanging' - ) givenLines = [ 'Hello world\n', '\u0d0a\u0a0d\u0d15\u0a15\r\n', @@ -606,6 +590,15 @@ def test_unicode(self, tmpdir, encoding): '\r', 'hanging', ] + given = ''.join(givenLines) + clean = ( + 'Hello world\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\u0d0a\u0a0d\u0d15\u0a15\n' + '\n' + 'hanging' + ) expectedLines = [ 'Hello world\n', '\u0d0a\u0a0d\u0d15\u0a15\n', From 31b992a752b124fbcecfcbaa62e6739d909b642d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 22:26:17 -0400 Subject: [PATCH 348/835] Use 'stripped' for clarity about difference from expectedLines --- test_path.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_path.py b/test_path.py index ad75ae70..75ff7220 100644 --- a/test_path.py +++ b/test_path.py @@ -607,7 +607,7 @@ def test_unicode(self, tmpdir, encoding): '\n', 'hanging', ] - expectedLines2 = [line.replace('\n', '') for line in expectedLines] + stripped = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file with io.open(p, 'w', encoding=encoding) as strm: @@ -618,7 +618,7 @@ def test_unicode(self, tmpdir, encoding): assert p.bytes() == given.encode(encoding) assert p.text(encoding) == clean assert p.lines(encoding) == expectedLines - assert p.lines(encoding, retain=False) == expectedLines2 + assert p.lines(encoding, retain=False) == stripped # If this is UTF-16, that's enough. # The rest of these will unfortunately fail because append=True @@ -640,12 +640,12 @@ def test_unicode(self, tmpdir, encoding): assert p.bytes() == expectedBytes assert p.text(encoding) == 2 * cleanNoHanging assert p.lines(encoding) == 2 * expectedLinesNoHanging - assert p.lines(encoding, retain=False) == 2 * expectedLines2 + assert p.lines(encoding, retain=False) == 2 * stripped # Write Unicode to file using path.write_lines(). # The output in the file should be exactly the same as last time. p.write_lines(expectedLines, encoding) - p.write_lines(expectedLines2, encoding, append=True) + p.write_lines(stripped, encoding, append=True) # Check the result. assert p.bytes() == expectedBytes From 4686eae1323dd39561a63b0efe345289d4c0e56e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 May 2020 22:30:24 -0400 Subject: [PATCH 349/835] Derive 'clean' from expectedLines --- test_path.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test_path.py b/test_path.py index 75ff7220..e785ec65 100644 --- a/test_path.py +++ b/test_path.py @@ -591,14 +591,6 @@ def test_unicode(self, tmpdir, encoding): 'hanging', ] given = ''.join(givenLines) - clean = ( - 'Hello world\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\u0d0a\u0a0d\u0d15\u0a15\n' - '\n' - 'hanging' - ) expectedLines = [ 'Hello world\n', '\u0d0a\u0a0d\u0d15\u0a15\n', @@ -607,6 +599,7 @@ def test_unicode(self, tmpdir, encoding): '\n', 'hanging', ] + clean = ''.join(expectedLines) stripped = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file From 7913605b375e49bc0ef7dc94bb006d11b159a2aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 10:37:02 -0400 Subject: [PATCH 350/835] Only suppress warnings on calls to .text. Ref #187. --- test_path.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test_path.py b/test_path.py index e785ec65..51e81419 100644 --- a/test_path.py +++ b/test_path.py @@ -572,7 +572,6 @@ def test_patterns(self, tmpdir): self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) - @pytest.mark.filterwarnings("ignore:.text is deprecated") @pytest.mark.parametrize("encoding", ('UTF-8', 'UTF-16BE', 'UTF-16LE', 'UTF-16')) def test_unicode(self, tmpdir, encoding): """ Test that path works with the specified encoding, @@ -609,7 +608,8 @@ def test_unicode(self, tmpdir, encoding): # test all 3 path read-fully functions, including # path.lines() in unicode mode. assert p.bytes() == given.encode(encoding) - assert p.text(encoding) == clean + with pytest.deprecated_call(): + assert p.text(encoding) == clean assert p.lines(encoding) == expectedLines assert p.lines(encoding, retain=False) == stripped @@ -631,7 +631,8 @@ def test_unicode(self, tmpdir, encoding): expectedLinesNoHanging = expectedLines[:] expectedLinesNoHanging[-1] += '\n' assert p.bytes() == expectedBytes - assert p.text(encoding) == 2 * cleanNoHanging + with pytest.deprecated_call(): + assert p.text(encoding) == 2 * cleanNoHanging assert p.lines(encoding) == 2 * expectedLinesNoHanging assert p.lines(encoding, retain=False) == 2 * stripped @@ -669,7 +670,8 @@ def testLinesep(eol): # Check the result. expectedBytes = 2 * given.encode(encoding) assert p.bytes() == expectedBytes - assert p.text(encoding) == 2 * clean + with pytest.deprecated_call(): + assert p.text(encoding) == 2 * clean expectedResultLines = expectedLines[:] expectedResultLines[-1] += expectedLines[0] expectedResultLines += expectedLines[1:] From b9a61aa8ebdd62592eda2e1b47ebcc8d21a12cb4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 10:21:37 -0400 Subject: [PATCH 351/835] Update .lines so it's no longer reliant on .text. Fixes #187. --- path/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 39561ec9..6335999f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -818,7 +818,8 @@ def lines(self, encoding=None, errors='strict', retain=True): .. seealso:: :meth:`text` """ - return self.text(encoding, errors).splitlines(retain) + text = U_NEWLINE.sub('\n', self.read_text(encoding, errors)) + return text.splitlines(retain) def write_lines( self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False From e7b746580c7062c53feed3993cd4c8d45b16fbae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 10:53:46 -0400 Subject: [PATCH 352/835] Update changelog. Ref #187. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4d5fa24c..fa6b2886 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v13.2.1 +------- + +- #187: ``lines()`` no longer relies on the deprecated ``.text()``. + v13.2.0 ------- From 172c52b03a77e70e92a1ee491b059464e938fde3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 11:31:51 -0400 Subject: [PATCH 353/835] Ensure virtualenv is upgraded when installing tox. Fixes jaraco/path#188. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 37fa499d..eed7b0a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,8 @@ python: cache: pip install: -- pip install tox +# ensure virtualenv is upgraded to avoid issues like jaraco/path#188 +- pip install -U --upgrade-strategy=eager tox before_script: # Enable IPv6. Ref travis-ci/travis-ci#8361 From 824d76e3a646f606785ebbaf4ee52d37aa5ab2df Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 13:52:11 -0400 Subject: [PATCH 354/835] Need to specify newline for tests to pass on Windows --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index b9f39841..bbf5b781 100644 --- a/test_path.py +++ b/test_path.py @@ -617,7 +617,7 @@ def test_unicode(self, tmpdir, encoding): expectedLines2 = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file - with io.open(p, 'w', encoding=encoding) as strm: + with io.open(p, 'w', encoding=encoding, newline='\n') as strm: strm.write(given) # test all 3 path read-fully functions, including From 7f65fc143648bead1a9fbd7b20906de46854e6b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 14:09:38 -0400 Subject: [PATCH 355/835] Write bytes manually as indicated. --- test_path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_path.py b/test_path.py index 19c186a5..da736f99 100644 --- a/test_path.py +++ b/test_path.py @@ -602,8 +602,8 @@ def test_unicode(self, tmpdir, encoding): stripped = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file - with io.open(p, 'w', encoding=encoding, newline='\n') as strm: - strm.write(given) + with io.open(p, 'wb') as strm: + strm.write(given.encode(encoding)) # test all 3 path read-fully functions, including # path.lines() in unicode mode. From 44d527eac6fe38f7790a318acf01b1a1e1c56c82 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 13:38:03 -0400 Subject: [PATCH 356/835] Prefer realpath to readlink. Fixes #186. --- test_path.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test_path.py b/test_path.py index da736f99..d93a4a25 100644 --- a/test_path.py +++ b/test_path.py @@ -475,9 +475,6 @@ def assertSetsEqual(self, a, b): assert ad == bd - @pytest.mark.xfail( - 'sys.version_info > (3, 8) and platform.system() == "Windows"', reason="#186" - ) def test_shutil(self, tmpdir): # Note: This only tests the methods exist and do roughly what # they should, neglecting the details as they are shutil's @@ -541,7 +538,7 @@ def test_shutil(self, tmpdir): ) if hasattr(os, 'symlink'): assert testCopyOfLink.islink() - assert testCopyOfLink.readlink() == testFile + assert testCopyOfLink.realpath() == testFile # Clean up. testDir.rmtree() From b8bdbb597342a615064282aed7bbb6d2b33b22dc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 14:51:59 -0400 Subject: [PATCH 357/835] Add backport of realpath for Python 3.7 and earlier on Windows. Allows tests to pass with the fix for #186. --- path/__init__.py | 4 +- path/py37compat.py | 118 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 path/py37compat.py diff --git a/path/__init__.py b/path/__init__.py index 6335999f..022bef13 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -55,6 +55,7 @@ pass from . import matchers +from .py37compat import best_realpath __all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] @@ -253,7 +254,8 @@ def normpath(self): def realpath(self): """ .. seealso:: :func:`os.path.realpath` """ - return self._next_class(self.module.realpath(self)) + realpath = best_realpath(self.module) + return self._next_class(realpath(self)) def expanduser(self): """ .. seealso:: :func:`os.path.expanduser` """ diff --git a/path/py37compat.py b/path/py37compat.py new file mode 100644 index 00000000..18b12e67 --- /dev/null +++ b/path/py37compat.py @@ -0,0 +1,118 @@ +import os +import sys + + +def best_realpath(module): + """ + Given a path module (i.e. ntpath, posixpath), + determine the best 'realpath' function to use + for best future compatibility. + """ + needs_backport = module.__name__ == 'ntpath' and sys.version_info < (3, 8) + return realpath_backport if needs_backport else module.realpath + + +# backport taken from jaraco.windows 5 +def realpath_backport(path): + if isinstance(path, str): + prefix = '\\\\?\\' + unc_prefix = prefix + 'UNC' + new_unc_prefix = '\\' + cwd = os.getcwd() + else: + prefix = b'\\\\?\\' + unc_prefix = prefix + b'UNC' + new_unc_prefix = b'\\' + cwd = os.getcwdb() + had_prefix = path.startswith(prefix) + path, ok = _resolve_path(cwd, path, {}) + # The path returned by _getfinalpathname will always start with \\?\ - + # strip off that prefix unless it was already provided on the original + # path. + if not had_prefix: + # For UNC paths, the prefix will actually be \\?\UNC - handle that + # case as well. + if path.startswith(unc_prefix): + path = new_unc_prefix + path[len(unc_prefix) :] + elif path.startswith(prefix): + path = path[len(prefix) :] + return path + + +def _resolve_path(path, rest, seen): + # Windows normalizes the path before resolving symlinks; be sure to + # follow the same behavior. + rest = os.path.normpath(rest) + + if isinstance(rest, str): + sep = '\\' + else: + sep = b'\\' + + if os.path.isabs(rest): + drive, rest = os.path.splitdrive(rest) + path = drive + sep + rest = rest[1:] + + while rest: + name, _, rest = rest.partition(sep) + new_path = os.path.join(path, name) if path else name + if os.path.exists(new_path): + if not rest: + # The whole path exists. Resolve it using the OS. + path = os.path._getfinalpathname(new_path) + else: + # The OS can resolve `new_path`; keep traversing the path. + path = new_path + elif not os.path.lexists(new_path): + # `new_path` does not exist on the filesystem at all. Use the + # OS to resolve `path`, if it exists, and then append the + # remainder. + if os.path.exists(path): + path = os.path._getfinalpathname(path) + rest = os.path.join(name, rest) if rest else name + return os.path.join(path, rest), True + else: + # We have a symbolic link that the OS cannot resolve. Try to + # resolve it ourselves. + + # On Windows, symbolic link resolution can be partially or + # fully disabled [1]. The end result of a disabled symlink + # appears the same as a broken symlink (lexists() returns True + # but exists() returns False). And in both cases, the link can + # still be read using readlink(). Call stat() and check the + # resulting error code to ensure we don't circumvent the + # Windows symbolic link restrictions. + # [1] https://technet.microsoft.com/en-us/library/cc754077.aspx + try: + os.stat(new_path) + except OSError as e: + # WinError 1463: The symbolic link cannot be followed + # because its type is disabled. + if e.winerror == 1463: + raise + + key = os.path.normcase(new_path) + if key in seen: + # This link has already been seen; try to use the + # previously resolved value. + path = seen[key] + if path is None: + # It has not yet been resolved, which means we must + # have a symbolic link loop. Return what we have + # resolved so far plus the remainder of the path (who + # cares about the Zen of Python?). + path = os.path.join(new_path, rest) if rest else new_path + return path, False + else: + # Mark this link as in the process of being resolved. + seen[key] = None + # Try to resolve it. + path, ok = _resolve_path(path, os.readlink(new_path), seen) + if ok: + # Resolution succeded; store the resolved value. + seen[key] = path + else: + # Resolution failed; punt. + return (os.path.join(path, rest) if rest else path), False + return path, True From 2b99984204606443b1a60b68066b7319e9ca5afa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 15:49:49 -0400 Subject: [PATCH 358/835] Update changelog. Ref #189. --- CHANGES.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fa6b2886..55a00a26 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ -v13.2.1 +v13.3.0 ------- +- #186: Fix test failures on Python 3.8 on Windows by relying on + ``realpath()`` instead of ``readlink()``. +- #189: ``realpath()`` now honors symlinks on Python 3.7 and + earlier, approximating the behavior found on Python 3.8. - #187: ``lines()`` no longer relies on the deprecated ``.text()``. v13.2.0 From 77b403381db3d9475dc7ed524c78f93263114df5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 16:41:10 -0400 Subject: [PATCH 359/835] Rely on module.abspath equivalence to detect need for backport. Co-authored-by: Steve Dower --- path/py37compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/py37compat.py b/path/py37compat.py index 18b12e67..b0df09fc 100644 --- a/path/py37compat.py +++ b/path/py37compat.py @@ -8,7 +8,7 @@ def best_realpath(module): determine the best 'realpath' function to use for best future compatibility. """ - needs_backport = module.__name__ == 'ntpath' and sys.version_info < (3, 8) + needs_backport = module.realpath is module.abspath return realpath_backport if needs_backport else module.realpath From 356dc72129871871f9341c747317a542264bf123 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 18:44:40 -0400 Subject: [PATCH 360/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/py37compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/path/py37compat.py b/path/py37compat.py index b0df09fc..9aa49b66 100644 --- a/path/py37compat.py +++ b/path/py37compat.py @@ -1,5 +1,4 @@ import os -import sys def best_realpath(module): From c764baf26c001ca5828fe99b8aa33aa55e9fd554 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 May 2020 23:17:49 -0400 Subject: [PATCH 361/835] Run tests on prereleases of Python on Windows. Fixes jaraco/skeleton#17. --- azure-pipelines.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fd432962..2be55e73 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,9 +29,12 @@ stages: python.version: '3.6' Bionic Python 3.8: python.version: '3.8' - Windows: + Windows Python 3.8: python.version: '3.8' pool_vm_image: vs2017-win2016 + Windows Python Prerelease: + python.version: '3.9' + pool_vm_image: vs2017-win2016 MacOS: python.version: '3.8' pool_vm_image: macos-10.15 @@ -39,10 +42,21 @@ stages: maxParallel: 4 steps: + - task: NuGetToolInstaller@1 + displayName: 'Install NuGet' + condition: eq(variables['pool_vm_image'], 'vs2017-win2016') + + - powershell: | + nuget install python -Prerelease -OutputDirectory "$(Build.BinariesDirectory)" -ExcludeVersion -NonInteractive + Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools" + Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools\Scripts" + condition: and(succeeded(), and(eq(variables['python.version'], '3.9'), eq(variables['pool_vm_image'], 'vs2017-win2016'))) + - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' architecture: 'x64' + condition: and(succeeded(), ne(variables['python.version'], '3.9')) - script: python -m pip install tox displayName: 'Install tox' From fc0162baafbd984f245de359b5a64c2d997a0714 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 May 2020 11:52:32 -0400 Subject: [PATCH 362/835] Add workaround for python/mypy#8627. Fixes jaraco/skeleton#18. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index e2dcebb1..0cf95493 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,8 @@ testing = pytest-black >= 0.3.7 pytest-cov pytest-mypy + # workaround for python/mypy#8627 + mypy@git+https://github.com/python/mypy # local From 814eb669c2e5436d8ebce1be845fa99e0687317b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 May 2020 12:17:23 -0400 Subject: [PATCH 363/835] Add 'refresh.svg' demonstrating an example of refreshing a project with the latest skeleton. Ref #7. --- docs/refresh.svg | 193 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 docs/refresh.svg diff --git a/docs/refresh.svg b/docs/refresh.svg new file mode 100644 index 00000000..04e62f00 --- /dev/null +++ b/docs/refresh.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + path master $ path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton From https://github.com/jaraco/skeleton * branch HEAD -> FETCH_HEADpath master $ path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH_ path master $ git diff ...FETCH_H path master $ git diff ...FETCH_HE path master $ git diff ...FETCH_HEA path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fd43296..2be55e7 100644--- a/azure-pipelines.yml+++ b/azure-pipelines.yml@@ -29,9 +29,12 @@ stages: python.version: '3.6' Bionic Python 3.8: python.version: '3.8'- Windows:+ Windows Python 3.8: pool_vm_image: vs2017-win2016+ Windows Python Prerelease:+ python.version: '3.9'+ pool_vm_image: vs2017-win2016 MacOS: pool_vm_image: macos-10.15@@ -39,10 +42,21 @@ stages: maxParallel: 4 steps:+ - task: NuGetToolInstaller@1+ displayName: 'Install NuGet'+ condition: eq(variables['pool_vm_image'], 'vs2017-win2016')++ - powershell: |+ nuget install python -Prerelease -OutputDi + nuget install python -Prerelease -OutputDirectory "$(Build.BinariesDirectory)" -ExcludeVersion -NonInteractive+ Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools"+ Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools\Scripts"+ condition: and(succeeded(), and(eq(variables['python.version'], '3.9'), eq(variables['pool_vm_image'], 'vs2017-win2016'))) - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' architecture: 'x64'+ condition: and(succeeded(), ne(variables['python.version'], '3.9')) - script: python -m pip install tox displayName: 'Install tox'diff --git a/setup.cfg b/setup.cfgindex e2dcebb..0cf9549 100644--- a/setup.cfg+++ b/setup.cfg@@ -29,6 +29,8 @@ testing = pytest-black >= 0.3.7 pytest-cov pytest-mypy+ # workaround for python/mypy#8627: + mypy@git+https://github.com/python/mypy # local(END) path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git push path master $ git push path master $ git pull path master $ git pull path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton Auto-merging setup.cfgAuto-merging azure-pipelines.ymlMerge made by the 'recursive' strategy. azure-pipelines.yml | 16 +++++++++++++++- setup.cfg | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-)path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git push path master $ git push path master $ git push Enumerating objects: 10, done. Counting objects: 50% (5/10)Counting objects: 100% (10/10), done.Delta compression using up to 8 threadsCompressing objects: 100% (4/4), done.Writing objects: 100% (4/4), 512 bytes | 512.00 KiB/s, done.Total 4 (delta 3), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To https://github.com/jaraco/path ff4d395..bd18026 master -> masterpath master $ + \ No newline at end of file From 169dad91b8316a7138bec2f7b51db80cab0aadc4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 May 2020 22:40:39 -0400 Subject: [PATCH 364/835] Move workaround for python/mypy#8627 to tox.ini, as adding it to setup.cfg prevents releases to PyPI. Fixes jaraco/skeleton#19. --- setup.cfg | 2 -- tox.ini | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0cf95493..e2dcebb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,6 @@ testing = pytest-black >= 0.3.7 pytest-cov pytest-mypy - # workaround for python/mypy#8627 - mypy@git+https://github.com/python/mypy # local diff --git a/tox.ini b/tox.ini index ba5857a3..97cc4261 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,8 @@ tox_pip_extensions_ext_venv_update = true [testenv] deps = + # workaround for python/mypy#8627 + mypy@git+https://github.com/python/mypy commands = pytest {posargs} usedevelop = True From 1b5b20ef62bcd849ece9b444369087fb7b60ac87 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:17:04 -0400 Subject: [PATCH 365/835] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 66a8916d..37ab86ca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v14.0.0 +------- + +- Removed ``FastPath``. Just use ``Path``. +- Removed ``path.CaseInsensitivePattern``. Instead + use ``path.matchers.CaseInsensitive``. + 11.5.0 ------ From 49749a6d8d47acbe3b5b014fc91b4ea6d495c6be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:23:31 -0400 Subject: [PATCH 366/835] Update changelog. --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ffc5869b..a344b0e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +v14.0.0 +------- + +- Removed ``update`` parameter on method to + ``Path.merge_tree``. Instead, to only copy newer files, + provide a wrapped ``copy`` function, as described in the + doc string. + 11.4.0 ------ From 53627d8b9476c5dd9c83e25fac2b00af25019687 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:30:19 -0400 Subject: [PATCH 367/835] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a9150c0d..f8edb8b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v14.0.0 +------- + +- Removed ``namebase`` property. Use ``stem`` instead. + 10.6 ---- From 6a63a1c7d367c0a0e9506bbb47717cd2b29f3429 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:32:50 -0400 Subject: [PATCH 368/835] Remove CaseInsensitivePattern from __all__. Ref #190. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 6f812f0f..247dbdc6 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -58,7 +58,7 @@ from .py37compat import best_realpath -__all__ = ['Path', 'TempDir', 'CaseInsensitivePattern'] +__all__ = ['Path', 'TempDir'] LINESEPS = ['\r\n', '\r', '\n'] From 67b8630d9e260cdac283c07d12b4443d219816f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:42:22 -0400 Subject: [PATCH 369/835] Fade to black --- path/__init__.py | 3 ++- test_path.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index cd2889f1..3c919070 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -138,6 +138,7 @@ class Traversal: `follow` is a function that takes an item and returns True if that item should be followed and False otherwise. """ + def __init__(self, follow): self.follow = follow @@ -598,7 +599,7 @@ def ignore(msg): for child in childList: traverse = None if match(child): - traverse = (yield child) + traverse = yield child traverse = traverse or child.isdir try: do_traverse = traverse() diff --git a/test_path.py b/test_path.py index 3c2a34cc..cb824562 100644 --- a/test_path.py +++ b/test_path.py @@ -256,9 +256,7 @@ def test_skip_symlinks(self, tmpdir): (sub / 'file').touch() assert len(list(root.walk())) == 4 - skip_links = path.Traversal( - lambda item: item.isdir() and not item.islink(), - ) + skip_links = path.Traversal(lambda item: item.isdir() and not item.islink(),) assert len(list(skip_links(root.walk()))) == 3 From 0a9e5a9af836e450ca5d30893998f6a3ac155b89 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:55:10 -0400 Subject: [PATCH 370/835] Enhance docstring --- path/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index 3c919070..2eeb2d0a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -137,6 +137,19 @@ class Traversal: `follow` is a function that takes an item and returns True if that item should be followed and False otherwise. + + For example, to avoid traversing into directories that + begin with `.`: + + >>> traverse = Traversal(lambda dir: not dir.startswith('.')) + >>> items = list(traverse(Path('.').walk())) + + Directories beginning with `.` will appear in the results, but + their children will not. + + >>> dot_dir = next(item for item in items if item.isdir() and item.startswith('.')) + >>> any(item.parent == dot_dir for item in items) + False """ def __init__(self, follow): From d1d01e5ce0e96d1c91e1dfc800f5ee145ab5c7d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:56:42 -0400 Subject: [PATCH 371/835] Update changelog. --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b06ad2d5..0d9971be 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ v14.0.0 - Removed ``FastPath``. Just use ``Path``. - Removed ``path.CaseInsensitivePattern``. Instead use ``path.matchers.CaseInsensitive``. +- Added ``Traversal`` class and support for customizing + the behavior of a ``Path.walk``. v13.3.0 ------- From 296ef9bde2f82ade44c873728dcb5c588022a509 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 04:58:34 -0400 Subject: [PATCH 372/835] Reference pertinent issue. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0d9971be..09c915d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,7 @@ v14.0.0 - Removed ``FastPath``. Just use ``Path``. - Removed ``path.CaseInsensitivePattern``. Instead use ``path.matchers.CaseInsensitive``. -- Added ``Traversal`` class and support for customizing +- #154: Added ``Traversal`` class and support for customizing the behavior of a ``Path.walk``. v13.3.0 From 4681bc2158772d312bc92d3ee734ae92a7f88e47 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 05:19:06 -0400 Subject: [PATCH 373/835] Remove path.tempdir alias. --- CHANGES.rst | 1 + path/__init__.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 09c915d3..d428c4b5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ v14.0.0 - Removed ``FastPath``. Just use ``Path``. - Removed ``path.CaseInsensitivePattern``. Instead use ``path.matchers.CaseInsensitive``. +- Removed ``path.tempdir``. Use ``path.TempDir``. - #154: Added ``Traversal`` class and support for customizing the behavior of a ``Path.walk``. diff --git a/path/__init__.py b/path/__init__.py index 2eeb2d0a..236cfa02 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1664,10 +1664,6 @@ def __exit__(self, exc_type, exc_value, traceback): self.rmtree() -# For backwards compatibility. -tempdir = TempDir - - def _multi_permission_mask(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. From 5ac761814a44939c1ef3fbbc28bb33f9cdb8ecaa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 05:25:46 -0400 Subject: [PATCH 374/835] Remove simple_cache, no longer needed. --- path/__init__.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 236cfa02..c5fab85a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -93,22 +93,6 @@ def compose(*funcs): return functools.reduce(compose_two, funcs) -def simple_cache(func): - """ - Save results for the :meth:'path.using_module' classmethod. - When Python 3.2 is available, use functools.lru_cache instead. - """ - saved_results = {} - - def wrapper(cls, module): - if module in saved_results: - return saved_results[module] - saved_results[module] = func(cls, module) - return saved_results[module] - - return wrapper - - class ClassProperty(property): def __get__(self, cls, owner): return self.fget.__get__(None, owner)() @@ -194,7 +178,7 @@ def __init__(self, other=''): raise TypeError("Invalid initial value for path: None") @classmethod - @simple_cache + @functools.lru_cache def using_module(cls, module): subclass_name = cls.__name__ + '_' + module.__name__ bases = (cls,) From ed6533e2010a0cc6764ab7986293bf07e5625d97 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 05:31:01 -0400 Subject: [PATCH 375/835] Remove more cruft --- path/__init__.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index c5fab85a..a4e731e5 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -98,7 +98,7 @@ def __get__(self, cls, owner): return self.fget.__get__(None, owner)() -class multimethod(object): +class multimethod: """ Acts like a classmethod when invoked from the class and like an instancemethod when invoked from the instance. @@ -1546,7 +1546,7 @@ def wrapper(src, dst, *args, **kwargs): return wrapper -class SpecialResolver(object): +class SpecialResolver: class ResolverScope: def __init__(self, paths, scope): self.paths = paths @@ -1558,10 +1558,6 @@ def __getattr__(self, class_): def __init__(self, path_class, *args, **kwargs): appdirs = importlib.import_module('appdirs') - # let appname default to None until - # https://github.com/ActiveState/appdirs/issues/55 is solved. - not args and kwargs.setdefault('appname', None) - vars(self).update( path_class=path_class, wrapper=appdirs.AppDirs(*args, **kwargs) ) From 71190e610647d43c4e5cae877479c7a0fc1e9079 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 05:32:54 -0400 Subject: [PATCH 376/835] Add docstrings to TempDir class --- path/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index a4e731e5..78324dd9 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1610,12 +1610,16 @@ class TempDir(Path): constructed with the same parameters that you can use as a context manager. - Example:: + For example: - with TempDir() as d: - # do stuff with the Path object "d" + >>> with TempDir() as d: + ... d.isdir() and isinstance(d, Path) + True + + The directory is deleted automatically. - # here the directory is deleted automatically + >>> d.isdir() + False .. seealso:: :func:`tempfile.mkdtemp` """ From 317f0111cdec720faadac68b520a211f2ecdf9c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 May 2020 05:52:07 -0400 Subject: [PATCH 377/835] Fix lru_cache usage on Python 3.7 --- CHANGES.rst | 6 ++++++ path/__init__.py | 4 ++-- path/py37compat.py | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d428c4b5..4f4163a7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v14.0.1 +------- + +- Fixed regression on Python 3.7 and earlier where ``lru_cache`` + did not support a user function. + v14.0.0 ------- diff --git a/path/__init__.py b/path/__init__.py index 78324dd9..ecb60acc 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -55,7 +55,7 @@ pass from . import matchers -from .py37compat import best_realpath +from .py37compat import best_realpath, lru_cache __all__ = ['Path', 'TempDir'] @@ -178,7 +178,7 @@ def __init__(self, other=''): raise TypeError("Invalid initial value for path: None") @classmethod - @functools.lru_cache + @lru_cache def using_module(cls, module): subclass_name = cls.__name__ + '_' + module.__name__ bases = (cls,) diff --git a/path/py37compat.py b/path/py37compat.py index 9aa49b66..0a769def 100644 --- a/path/py37compat.py +++ b/path/py37compat.py @@ -1,3 +1,4 @@ +import functools import os @@ -115,3 +116,10 @@ def _resolve_path(path, rest, seen): # Resolution failed; punt. return (os.path.join(path, rest) if rest else path), False return path, True + + +def lru_cache(user_function): + """ + Support for lru_cache(user_function) + """ + return functools.lru_cache()(user_function) From a2fc27929fe7f7d6dc87700560d58abd08ddad46 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Jul 2020 03:33:59 -0400 Subject: [PATCH 378/835] Remove workaround for python/mypy#8627. Ref jaraco/skeleton#18. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 97cc4261..ba5857a3 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,6 @@ tox_pip_extensions_ext_venv_update = true [testenv] deps = - # workaround for python/mypy#8627 - mypy@git+https://github.com/python/mypy commands = pytest {posargs} usedevelop = True From cfc337705e54e68d17ba498f0b2b69110692a8d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Jul 2020 19:29:42 -0400 Subject: [PATCH 379/835] Remove __version__. --- CHANGES.rst | 6 ++++++ path/__init__.py | 14 -------------- setup.cfg | 1 - test_path.py | 8 -------- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4f4163a7..1ba2d408 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v15.0.0 +------- + +- Removed ``__version__`` property. To determine the version, + use ``importlib.metadata.version('path')``. + v14.0.1 ------- diff --git a/path/__init__.py b/path/__init__.py index ecb60acc..e688d859 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -69,20 +69,6 @@ U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern)) -try: - from importlib import metadata # type: ignore -except ImportError: - try: - import importlib_metadata as metadata # type: ignore - except ImportError: - pass - -try: - __version__ = metadata.version('path') # type: ignore -except Exception: - __version__ = 'unknown' - - class TreeWalkWarning(Warning): pass diff --git a/setup.cfg b/setup.cfg index f6aa05d5..29e19f02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,6 @@ packages = find: include_package_data = true python_requires = >=3.6 install_requires = - importlib_metadata >= 0.5; python_version < "3.8" setup_requires = setuptools_scm[toml] >= 3.4.1 [options.extras_require] diff --git a/test_path.py b/test_path.py index cb824562..59f8660c 100644 --- a/test_path.py +++ b/test_path.py @@ -1232,11 +1232,3 @@ def test_no_dependencies(): """ cmd = [sys.executable, '-S', '-c', 'import path'] subprocess.check_call(cmd) - - -def test_version(): - """ - Under normal circumstances, path should present a - __version__. - """ - assert re.match(r'\d+\.\d+.*', path.__version__) From 9f075d307e60fbad8be218edfb88bb0d62300a65 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Jul 2020 19:49:37 -0400 Subject: [PATCH 380/835] Unpin shim to avoid conflicts. Fixes #195. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 38e52094..64cc8ca1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = find: include_package_data = true python_requires = >=3.5 install_requires = - path < 13.2 + path setup_requires = setuptools_scm >= 1.15.0 [options.extras_require] From cbf8d29b4ce00b0001d138d2f233e37a28a8e10d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 27 Jul 2020 19:50:39 -0400 Subject: [PATCH 381/835] Update changelog. Ref #195. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 24c78916..050cab1c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v12.5.0 +------- + +- #195: Project now depends on ``path``. + v12.4.0 ------- From 616427a0ade74c2bd03f8afe10dc1faee98c0b79 Mon Sep 17 00:00:00 2001 From: Dimitri Merejkowsky Date: Mon, 24 Aug 2020 16:17:02 +0200 Subject: [PATCH 382/835] test_list_dir_other_encoding: replace `skip` with `skipif` The `skip` markers always skips the test, here we still want the test to run when on Linux. --- test_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index 59f8660c..b9685f71 100644 --- a/test_path.py +++ b/test_path.py @@ -415,7 +415,7 @@ def test_listing(self, tmpdir): except Exception: pass - @pytest.mark.skip( + @pytest.mark.skipif( platform.system() != "Linux", reason="Only Linux allows writing invalid encodings", ) From e630a0c8f213e4013b57a695187755641f7537a3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Aug 2020 14:30:41 -0400 Subject: [PATCH 383/835] Create Github releases when releasing the package. Fixes jaraco/skeleton#23. --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index ba5857a3..7347d95f 100644 --- a/tox.ini +++ b/tox.ini @@ -26,11 +26,14 @@ deps = pep517>=0.5 twine[keyring]>=1.13 path + jaraco.develop>=7.1 passenv = TWINE_PASSWORD + GITHUB_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import path; path.Path('dist').rmtree_p()" python -m pep517.build . python -m twine upload dist/* + python -m jaraco.develop.create-github-release From 8032e1d3fb114bb93736d5b6668adc4d44f6c00b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Aug 2020 15:26:29 -0400 Subject: [PATCH 384/835] Moved refresh.svg to another branch. Reference the animation from the docs. Ref jaraco/skeleton#7. --- docs/refresh.svg | 193 ----------------------------------------------- skeleton.md | 4 + 2 files changed, 4 insertions(+), 193 deletions(-) delete mode 100644 docs/refresh.svg diff --git a/docs/refresh.svg b/docs/refresh.svg deleted file mode 100644 index 04e62f00..00000000 --- a/docs/refresh.svg +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - - - - path master $ path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton From https://github.com/jaraco/skeleton * branch HEAD -> FETCH_HEADpath master $ path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git fetch gh://jaraco/skeleton path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH path master $ git diff ...FETCH_ path master $ git diff ...FETCH_H path master $ git diff ...FETCH_HE path master $ git diff ...FETCH_HEA path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fd43296..2be55e7 100644--- a/azure-pipelines.yml+++ b/azure-pipelines.yml@@ -29,9 +29,12 @@ stages: python.version: '3.6' Bionic Python 3.8: python.version: '3.8'- Windows:+ Windows Python 3.8: pool_vm_image: vs2017-win2016+ Windows Python Prerelease:+ python.version: '3.9'+ pool_vm_image: vs2017-win2016 MacOS: pool_vm_image: macos-10.15@@ -39,10 +42,21 @@ stages: maxParallel: 4 steps:+ - task: NuGetToolInstaller@1+ displayName: 'Install NuGet'+ condition: eq(variables['pool_vm_image'], 'vs2017-win2016')++ - powershell: |+ nuget install python -Prerelease -OutputDi + nuget install python -Prerelease -OutputDirectory "$(Build.BinariesDirectory)" -ExcludeVersion -NonInteractive+ Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools"+ Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools\Scripts"+ condition: and(succeeded(), and(eq(variables['python.version'], '3.9'), eq(variables['pool_vm_image'], 'vs2017-win2016'))) - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' architecture: 'x64'+ condition: and(succeeded(), ne(variables['python.version'], '3.9')) - script: python -m pip install tox displayName: 'Install tox'diff --git a/setup.cfg b/setup.cfgindex e2dcebb..0cf9549 100644--- a/setup.cfg+++ b/setup.cfg@@ -29,6 +29,8 @@ testing = pytest-black >= 0.3.7 pytest-cov pytest-mypy+ # workaround for python/mypy#8627: + mypy@git+https://github.com/python/mypy # local(END) path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git diff ...FETCH_HEAD path master $ git push path master $ git push path master $ git pull path master $ git pull path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton Auto-merging setup.cfgAuto-merging azure-pipelines.ymlMerge made by the 'recursive' strategy. azure-pipelines.yml | 16 +++++++++++++++- setup.cfg | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-)path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git pull gh://jaraco/skeleton path master $ git push path master $ git push path master $ git push Enumerating objects: 10, done. Counting objects: 50% (5/10)Counting objects: 100% (10/10), done.Delta compression using up to 8 threadsCompressing objects: 100% (4/4), done.Writing objects: 100% (4/4), 512 bytes | 512.00 KiB/s, done.Total 4 (delta 3), reused 0 (delta 0), pack-reused 0remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To https://github.com/jaraco/path ff4d395..bd18026 master -> masterpath master $ - \ No newline at end of file diff --git a/skeleton.md b/skeleton.md index 4544158f..17a94ed7 100644 --- a/skeleton.md +++ b/skeleton.md @@ -40,6 +40,10 @@ The `--allow-unrelated-histories` is necessary because the history from the skel Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar Git operations. +For example, here's a session of the [path project](https://pypi.org/project/path) pulling non-conflicting changes from the skeleton: + + + Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. # Features From 6a13942eff3b62a7b4017101c33ae752e69fbc89 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Sep 2020 22:00:43 -0400 Subject: [PATCH 385/835] Add the env var mapping too. --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2be55e73..fdad0e5d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -89,6 +89,7 @@ stages: tox -e release env: TWINE_PASSWORD: $(PyPI-token) + GITHUB_TOKEN: $(Github-token) displayName: 'publish to PyPI' condition: contains(variables['Build.SourceBranch'], 'tags') From 15f6272d1eb253940f82737b4f340365ee9879a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Sep 2020 18:33:17 -0400 Subject: [PATCH 386/835] Disable pytest-black and pytest-mypy on PyPy. Fixes jaraco/skeleton#22. Ref pytest-dev/pytest#7675. --- pyproject.toml | 8 ++++++++ setup.cfg | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ee7df23..9b02ee75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,11 @@ build-backend = "setuptools.build_meta" skip-string-normalization = true [tool.setuptools_scm] + +# jaraco/skeleton#22 +[tool.jaraco.pytest.opts.--black] +action = "store_true" + +# jaraco/skeleton#22 +[tool.jaraco.pytest.opts.--mypy] +action = "store_true" diff --git a/setup.cfg b/setup.cfg index e2dcebb1..e9dd1772 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,9 +26,11 @@ testing = pytest >= 3.5, !=3.7.3 pytest-checkdocs >= 1.2.3 pytest-flake8 - pytest-black >= 0.3.7 + pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov - pytest-mypy + pytest-mypy; python_implementation != "PyPy" + # jaraco/skeleton#22 + jaraco.test >= 3 # local From 38207546aea22e38433a316c6ad9bf024de61ef3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Sep 2020 19:55:53 -0400 Subject: [PATCH 387/835] Bump black and blacken-docs to latest stable versions. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe46b8c5..6639c78c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: 19.10b0 + rev: stable hooks: - id: black - repo: https://github.com/asottile/blacken-docs - rev: v1.4.0 + rev: v1.8.0 hooks: - id: blacken-docs From 8117892ea5da9969f49756933bcbaa576ee7c5d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Sep 2020 14:18:23 -0400 Subject: [PATCH 388/835] Use enabled plugin configuration to enable mypy and black when the plugin is present. Ref jaraco/skeleton#22. --- pyproject.toml | 8 ++++---- pytest.ini | 2 +- setup.cfg | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b02ee75..9cd13ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ skip-string-normalization = true [tool.setuptools_scm] # jaraco/skeleton#22 -[tool.jaraco.pytest.opts.--black] -action = "store_true" +[tool.jaraco.pytest.plugins.black] +addopts = "--black" # jaraco/skeleton#22 -[tool.jaraco.pytest.opts.--mypy] -action = "store_true" +[tool.jaraco.pytest.plugins.mypy] +addopts = "--mypy" diff --git a/pytest.ini b/pytest.ini index 381b3271..5ffd7f73 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 --black --cov --mypy +addopts=--doctest-modules --flake8 --cov doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 diff --git a/setup.cfg b/setup.cfg index e9dd1772..eb834fef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ testing = pytest-cov pytest-mypy; python_implementation != "PyPy" # jaraco/skeleton#22 - jaraco.test >= 3 + jaraco.test >= 3.1.1 # local From 678e1a973a0139c0e0ab40395dfbada6c3ea72b9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Sep 2020 19:02:55 -0400 Subject: [PATCH 389/835] Also enable flake8 and cov when the plugins are present. --- pyproject.toml | 6 ++++++ pytest.ini | 2 +- setup.cfg | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9cd13ba4..79f088a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,9 @@ addopts = "--black" # jaraco/skeleton#22 [tool.jaraco.pytest.plugins.mypy] addopts = "--mypy" + +[tool.jaraco.pytest.plugins.flake8] +addopts = "--flake8" + +[tool.jaraco.pytest.plugins.cov] +addopts = "--cov" diff --git a/pytest.ini b/pytest.ini index 5ffd7f73..d7f0b115 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules --flake8 --cov +addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 diff --git a/setup.cfg b/setup.cfg index eb834fef..6321ca77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ testing = pytest-cov pytest-mypy; python_implementation != "PyPy" # jaraco/skeleton#22 - jaraco.test >= 3.1.1 + jaraco.test >= 3.2.0 # local From 5a347957ccb8727428ddfe8f6b3a4b508501a558 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 13 Sep 2020 20:17:51 -0400 Subject: [PATCH 390/835] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- path/__init__.py | 137 ++++++++++++++++++++-------------------- test_path.py | 10 +-- 3 files changed, 75 insertions(+), 74 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6639c78c..af0dc38b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: stable + rev: 20.8b1 hooks: - id: black diff --git a/path/__init__.py b/path/__init__.py index e688d859..12e63ee0 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -198,7 +198,7 @@ def __radd__(self, other): # The / operator joins Paths. def __div__(self, rel): - """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) + """fp.__div__(rel) == fp / rel == fp.joinpath(rel) Join two path components, adding a separator character if needed. @@ -212,7 +212,7 @@ def __div__(self, rel): # The / operator joins Paths the other way around def __rdiv__(self, rel): - """ fp.__rdiv__(rel) == rel / fp + """fp.__rdiv__(rel) == rel / fp Join two path components, adding a separator character if needed. @@ -237,7 +237,7 @@ def __fspath__(self): @classmethod def getcwd(cls): - """ Return the current working directory as a path object. + """Return the current working directory as a path object. .. seealso:: :func:`os.getcwdu` """ @@ -280,7 +280,7 @@ def basename(self): return self._next_class(self.module.basename(self)) def expand(self): - """ Clean up a filename by calling :meth:`expandvars()`, + """Clean up a filename by calling :meth:`expandvars()`, :meth:`expanduser()`, and :meth:`normpath()` on it. This is commonly everything needed to clean up a filename @@ -290,7 +290,7 @@ def expand(self): @property def stem(self): - """ The same as :meth:`name`, but with one file extension stripped off. + """The same as :meth:`name`, but with one file extension stripped off. >>> Path('/home/guido/python.tar.gz').stem 'python.tar' @@ -305,7 +305,7 @@ def ext(self): return ext def with_suffix(self, suffix): - """ Return a new path with the file suffix changed (or added, if none) + """Return a new path with the file suffix changed (or added, if none) >>> Path('/home/guido/python.tar.gz').with_suffix(".foo") Path('/home/guido/python.tar.foo') @@ -325,7 +325,7 @@ def with_suffix(self, suffix): @property def drive(self): - """ The drive specifier, for example ``'C:'``. + """The drive specifier, for example ``'C:'``. This is always empty on systems that don't use drive specifiers. """ @@ -360,7 +360,7 @@ def drive(self): ) def splitpath(self): - """ p.splitpath() -> Return ``(p.parent, p.name)``. + """p.splitpath() -> Return ``(p.parent, p.name)``. .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split` """ @@ -368,7 +368,7 @@ def splitpath(self): return self._next_class(parent), child def splitdrive(self): - """ p.splitdrive() -> Return ``(p.drive, )``. + """p.splitdrive() -> Return ``(p.drive, )``. Split the drive specifier from this path. If there is no drive specifier, :samp:`{p.drive}` is empty, so the return value @@ -380,7 +380,7 @@ def splitdrive(self): return self._next_class(drive), rel def splitext(self): - """ p.splitext() -> Return ``(p.stripext(), p.ext)``. + """p.splitext() -> Return ``(p.stripext(), p.ext)``. Split the filename extension from this path and return the two parts. Either part may be empty. @@ -395,7 +395,7 @@ def splitext(self): return self._next_class(filename), ext def stripext(self): - """ p.stripext() -> Remove one file extension from the path. + """p.stripext() -> Remove one file extension from the path. For example, ``Path('/home/guido/python.tar.gz').stripext()`` returns ``Path('/home/guido/python.tar')``. @@ -431,7 +431,7 @@ def joinpath(cls, first, *others): return first._next_class(first.module.join(first, *others)) def splitall(self): - r""" Return a list of the path components in this path. + r"""Return a list of the path components in this path. The first item in the list will be a Path. Its value will be either :data:`os.curdir`, :data:`os.pardir`, empty, or the root @@ -453,14 +453,14 @@ def splitall(self): return parts def relpath(self, start='.'): - """ Return this path as a relative path, + """Return this path as a relative path, based from `start`, which defaults to the current working directory. """ cwd = self._next_class(start) return cwd.relpathto(self) def relpathto(self, dest): - """ Return a relative path from `self` to `dest`. + """Return a relative path from `self` to `dest`. If there is no relative path from `self` to `dest`, for example if they reside on different drives in Windows, then this returns @@ -500,7 +500,7 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching def listdir(self, match=None): - """ D.listdir() -> List of items in this directory. + """D.listdir() -> List of items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing of just files or just subdirectories. @@ -516,7 +516,7 @@ def listdir(self, match=None): return list(filter(match, (self / child for child in os.listdir(self)))) def dirs(self, *args, **kwargs): - """ D.dirs() -> List of this directory's subdirectories. + """D.dirs() -> List of this directory's subdirectories. The elements of the list are Path objects. This does not walk recursively into subdirectories @@ -527,7 +527,7 @@ def dirs(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isdir()] def files(self, *args, **kwargs): - """ D.files() -> List of the files in this directory. + """D.files() -> List of the files in this directory. The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). @@ -538,7 +538,7 @@ def files(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isfile()] def walk(self, match=None, errors='strict'): - """ D.walk() -> iterator over files and subdirs, recursively. + """D.walk() -> iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of this directory and its descendants. This requires that @@ -598,17 +598,15 @@ def ignore(msg): yield item def walkdirs(self, *args, **kwargs): - """ D.walkdirs() -> iterator over subdirs, recursively. - """ + """D.walkdirs() -> iterator over subdirs, recursively.""" return (item for item in self.walk(*args, **kwargs) if item.isdir()) def walkfiles(self, *args, **kwargs): - """ D.walkfiles() -> iterator over files in D, recursively. - """ + """D.walkfiles() -> iterator over files in D, recursively.""" return (item for item in self.walk(*args, **kwargs) if item.isfile()) def fnmatch(self, pattern, normcase=None): - """ Return ``True`` if `self.name` matches the given `pattern`. + """Return ``True`` if `self.name` matches the given `pattern`. `pattern` - A filename pattern with wildcards, for example ``'*.py'``. If the pattern contains a `normcase` @@ -627,7 +625,7 @@ def fnmatch(self, pattern, normcase=None): return fnmatch.fnmatchcase(name, pattern) def glob(self, pattern): - """ Return a list of Path objects that match the pattern. + """Return a list of Path objects that match the pattern. `pattern` - a path relative to this directory, with wildcards. @@ -644,7 +642,7 @@ def glob(self, pattern): return [cls(s) for s in glob.glob(self / pattern)] def iglob(self, pattern): - """ Return an iterator of Path objects that match the pattern. + """Return an iterator of Path objects that match the pattern. `pattern` - a path relative to this directory, with wildcards. @@ -665,7 +663,7 @@ def iglob(self, pattern): # --- Reading or writing an entire file at once. def open(self, *args, **kwargs): - """ Open this file and return a corresponding :class:`file` object. + """Open this file and return a corresponding :class:`file` object. Keyword arguments work as in :func:`io.open`. If the file cannot be opened, an :class:`~exceptions.OSError` is raised. @@ -678,25 +676,25 @@ def bytes(self): return f.read() def chunks(self, size, *args, **kwargs): - """ Returns a generator yielding chunks of the file, so it can - be read piece by piece with a simple for loop. + """Returns a generator yielding chunks of the file, so it can + be read piece by piece with a simple for loop. - Any argument you pass after `size` will be passed to :meth:`open`. + Any argument you pass after `size` will be passed to :meth:`open`. - :example: + :example: - >>> hash = hashlib.md5() - >>> for chunk in Path("CHANGES.rst").chunks(8192, mode='rb'): - ... hash.update(chunk) + >>> hash = hashlib.md5() + >>> for chunk in Path("CHANGES.rst").chunks(8192, mode='rb'): + ... hash.update(chunk) - This will read the file by chunks of 8192 bytes. + This will read the file by chunks of 8192 bytes. """ with self.open(*args, **kwargs) as f: for chunk in iter(lambda: f.read(size) or None, None): yield chunk def write_bytes(self, bytes, append=False): - """ Open this file and write the given bytes to it. + """Open this file and write the given bytes to it. Default behavior is to overwrite any existing file. Call ``p.write_bytes(bytes, append=True)`` to append instead. @@ -709,7 +707,7 @@ def write_bytes(self, bytes, append=False): f.write(bytes) def read_text(self, encoding=None, errors=None): - r""" Open this file, read it in, return the content as a string. + r"""Open this file, read it in, return the content as a string. Optional parameters are passed to :meth:`open`. @@ -734,7 +732,7 @@ def text(self, encoding=None, errors='strict'): def write_text( self, text, encoding=None, errors='strict', linesep=os.linesep, append=False ): - r""" Write the given text to this file. + r"""Write the given text to this file. The default behavior is to overwrite any existing file; to append instead, use the `append=True` keyword argument. @@ -807,7 +805,7 @@ def write_text( self.write_bytes(text, append=append) def lines(self, encoding=None, errors='strict', retain=True): - r""" Open this file, read all lines, return them in a list. + r"""Open this file, read all lines, return them in a list. Optional arguments: `encoding` - The Unicode encoding (or character set) of @@ -829,7 +827,7 @@ def lines(self, encoding=None, errors='strict', retain=True): def write_lines( self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False ): - r""" Write the given lines of text to this file. + r"""Write the given lines of text to this file. By default this overwrites any existing file at this path. @@ -876,7 +874,7 @@ def write_lines( f.write(line) def read_md5(self): - """ Calculate the md5 hash for this file. + """Calculate the md5 hash for this file. This reads through the entire file. @@ -885,7 +883,7 @@ def read_md5(self): return self.read_hash('md5') def _hash(self, hash_name): - """ Returns a hash object for the file at the current path. + """Returns a hash object for the file at the current path. `hash_name` should be a hash algo name (such as ``'md5'`` or ``'sha1'``) that's available in the :mod:`hashlib` module. @@ -896,7 +894,7 @@ def _hash(self, hash_name): return m def read_hash(self, hash_name): - """ Calculate given hash for this file. + """Calculate given hash for this file. List of supported hashes can be obtained from :mod:`hashlib` package. This reads the entire file. @@ -906,7 +904,7 @@ def read_hash(self, hash_name): return self._hash(hash_name).digest() def read_hexhash(self, hash_name): - """ Calculate given hash for this file, returning hexdigest. + """Calculate given hash for this file, returning hexdigest. List of supported hashes can be obtained from :mod:`hashlib` package. This reads the entire file. @@ -1010,7 +1008,7 @@ def getsize(self): if hasattr(os, 'access'): def access(self, mode): - """ Return ``True`` if current user has access to this path. + """Return ``True`` if current user has access to this path. mode - One of the constants :data:`os.F_OK`, :data:`os.R_OK`, :data:`os.W_OK`, :data:`os.X_OK` @@ -1020,14 +1018,14 @@ def access(self, mode): return os.access(self, mode) def stat(self): - """ Perform a ``stat()`` system call on this path. + """Perform a ``stat()`` system call on this path. .. seealso:: :meth:`lstat`, :func:`os.stat` """ return os.stat(self) def lstat(self): - """ Like :meth:`stat`, but do not follow symbolic links. + """Like :meth:`stat`, but do not follow symbolic links. .. seealso:: :meth:`stat`, :func:`os.lstat` """ @@ -1081,7 +1079,7 @@ def __get_owner_not_implemented(self): if hasattr(os, 'statvfs'): def statvfs(self): - """ Perform a ``statvfs()`` system call on this path. + """Perform a ``statvfs()`` system call on this path. .. seealso:: :func:`os.statvfs` """ @@ -1097,7 +1095,7 @@ def pathconf(self, name): # --- Modifying operations on files and directories def utime(self, times): - """ Set the access and modified times of this file. + """Set the access and modified times of this file. .. seealso:: :func:`os.utime` """ @@ -1153,8 +1151,8 @@ def mkdir(self, mode=0o777): return self def mkdir_p(self, mode=0o777): - """ Like :meth:`mkdir`, but does not raise an exception if the - directory already exists. """ + """Like :meth:`mkdir`, but does not raise an exception if the + directory already exists.""" with contextlib.suppress(FileExistsError): self.mkdir(mode) return self @@ -1165,8 +1163,8 @@ def makedirs(self, mode=0o777): return self def makedirs_p(self, mode=0o777): - """ Like :meth:`makedirs`, but does not raise an exception if the - directory already exists. """ + """Like :meth:`makedirs`, but does not raise an exception if the + directory already exists.""" with contextlib.suppress(FileExistsError): self.makedirs(mode) return self @@ -1177,8 +1175,8 @@ def rmdir(self): return self def rmdir_p(self): - """ Like :meth:`rmdir`, but does not raise an exception if the - directory is not empty or does not exist. """ + """Like :meth:`rmdir`, but does not raise an exception if the + directory is not empty or does not exist.""" suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty with contextlib.suppress(suppressed): with DirectoryNotEmpty.translate(): @@ -1191,8 +1189,8 @@ def removedirs(self): return self def removedirs_p(self): - """ Like :meth:`removedirs`, but does not raise an exception if the - directory is not empty or does not exist. """ + """Like :meth:`removedirs`, but does not raise an exception if the + directory is not empty or does not exist.""" with contextlib.suppress(FileExistsError, DirectoryNotEmpty): with DirectoryNotEmpty.translate(): self.removedirs() @@ -1201,7 +1199,7 @@ def removedirs_p(self): # --- Modifying operations on files def touch(self): - """ Set the access/modified times of this file to the current time. + """Set the access/modified times of this file to the current time. Create the file if it does not exist. """ fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) @@ -1215,8 +1213,8 @@ def remove(self): return self def remove_p(self): - """ Like :meth:`remove`, but does not raise an exception if the - file does not exist. """ + """Like :meth:`remove`, but does not raise an exception if the + file does not exist.""" with contextlib.suppress(FileNotFoundError): self.unlink() return self @@ -1227,8 +1225,8 @@ def unlink(self): return self def unlink_p(self): - """ Like :meth:`unlink`, but does not raise an exception if the - file does not exist. """ + """Like :meth:`unlink`, but does not raise an exception if the + file does not exist.""" self.remove_p() return self @@ -1237,7 +1235,7 @@ def unlink_p(self): if hasattr(os, 'link'): def link(self, newpath): - """ Create a hard link at `newpath`, pointing to this file. + """Create a hard link at `newpath`, pointing to this file. .. seealso:: :func:`os.link` """ @@ -1247,7 +1245,7 @@ def link(self, newpath): if hasattr(os, 'symlink'): def symlink(self, newlink=None): - """ Create a symbolic link at `newlink`, pointing here. + """Create a symbolic link at `newlink`, pointing here. If newlink is not supplied, the symbolic link will assume the name self.basename(), creating the link in the cwd. @@ -1262,7 +1260,7 @@ def symlink(self, newlink=None): if hasattr(os, 'readlink'): def readlink(self): - """ Return the path to which this symbolic link points. + """Return the path to which this symbolic link points. The result may be an absolute or a relative path. @@ -1271,7 +1269,7 @@ def readlink(self): return self._next_class(os.readlink(self)) def readlinkabs(self): - """ Return the path to which this symbolic link points. + """Return the path to which this symbolic link points. The result is always an absolute path. @@ -1298,8 +1296,8 @@ def readlinkabs(self): rmtree = shutil.rmtree def rmtree_p(self): - """ Like :meth:`rmtree`, but does not raise an exception if the - directory does not exist. """ + """Like :meth:`rmtree`, but does not raise an exception if the + directory does not exist.""" with contextlib.suppress(FileNotFoundError): self.rmtree() return self @@ -1347,7 +1345,10 @@ def ignored(item): target.symlink(dest) elif source.isdir(): source.merge_tree( - dest, symlinks=symlinks, copy_function=copy_function, ignore=ignore, + dest, + symlinks=symlinks, + copy_function=copy_function, + ignore=ignore, ) else: copy_function(source, dest) diff --git a/test_path.py b/test_path.py index 59f8660c..29504a84 100644 --- a/test_path.py +++ b/test_path.py @@ -89,9 +89,7 @@ def test_relpath(self): assert d.relpathto(boz) == boz def test_construction_from_none(self): - """ - - """ + """""" try: Path(None) except TypeError: @@ -256,7 +254,9 @@ def test_skip_symlinks(self, tmpdir): (sub / 'file').touch() assert len(list(root.walk())) == 4 - skip_links = path.Traversal(lambda item: item.isdir() and not item.islink(),) + skip_links = path.Traversal( + lambda item: item.isdir() and not item.islink(), + ) assert len(list(skip_links(root.walk()))) == 3 @@ -584,7 +584,7 @@ def test_patterns(self, tmpdir): @pytest.mark.parametrize("encoding", ('UTF-8', 'UTF-16BE', 'UTF-16LE', 'UTF-16')) def test_unicode(self, tmpdir, encoding): - """ Test that path works with the specified encoding, + """Test that path works with the specified encoding, which must be capable of representing the entire range of Unicode codepoints. """ From ea912cf6598bd882a723518eb5cc01f1c8397094 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 Oct 2020 18:09:29 -0400 Subject: [PATCH 391/835] Add workflows for running tests. Ref jaraco/skeleton#24. --- .github/workflows/main.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a1897983 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,22 @@ +name: Main + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: [3.6, 3.7, 3.8] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox From ce34be2f7548dd68f74e5a0fb98f5b796b076900 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 Oct 2020 19:55:49 -0400 Subject: [PATCH 392/835] Cut releases from Github Actions instead of Azure Pipelines. Ref jaraco/skeleton#24. --- .github/workflows/main.yml | 22 +++++++++++++++++++++- azure-pipelines.yml | 23 ----------------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a1897983..b3dd81fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Main +name: Automated Tests on: [push, pull_request] @@ -20,3 +20,23 @@ jobs: python -m pip install tox - name: Run tests run: tox + + release: + needs: test + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fdad0e5d..6d318994 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -70,26 +70,3 @@ stages: testResultsFiles: '**/test-results.xml' testRunTitle: 'Python $(python.version)' condition: succeededOrFailed() - -- stage: Publish - dependsOn: Test - jobs: - - job: 'Publish' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.8' - architecture: 'x64' - - - script: python -m pip install tox - displayName: 'Install tox' - - - script: | - tox -e release - env: - TWINE_PASSWORD: $(PyPI-token) - GITHUB_TOKEN: $(Github-token) - displayName: 'publish to PyPI' - - condition: contains(variables['Build.SourceBranch'], 'tags') From dcd7cbda05754898cc9723c9b36e41a92cb3e139 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Oct 2020 06:12:09 -0400 Subject: [PATCH 393/835] Refresh docs to prefer Github Actions to Azure Pipelines. Ref jaraco/skeleton#24. --- skeleton.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/skeleton.md b/skeleton.md index 17a94ed7..c6b0cd0c 100644 --- a/skeleton.md +++ b/skeleton.md @@ -56,6 +56,7 @@ The features/techniques employed by the skeleton include: - A README.rst as reStructuredText with some popular badges, but with Read the Docs and AppVeyor badges commented out - A CHANGES.rst file intended for publishing release notes about the project - Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) +- Integrated type checking through [mypy](https://github.com/python/mypy/). ## Packaging Conventions @@ -109,9 +110,20 @@ Relies on a .flake8 file to correct some default behaviors: The project is pre-configured to run tests through multiple CI providers. +### Github Actions + +[Github Actions](https://docs.github.com/en/free-pro-team@latest/actions) are the preferred provider as they provide free, fast, multi-platform services with straightforward configuration. Configured in `.github/workflows`. + +Features include: +- test against multiple Python versions +- run on late (and updated) platform versions +- automated releases of tagged commits + ### Azure Pipelines -[Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) are the preferred provider as they provide free, fast, multi-platform services. See azure-pipelines.yml for more details. +[Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) were adopted for free, fast, multi-platform services. See azure-pipelines.yml for more details. + +Azure Pipelines require many [complicated setup steps](https://github.com/Azure/azure-devops-cli-extension/issues/968) that have not been readily automated. Features include: @@ -133,20 +145,13 @@ A minimal template for running under AppVeyor (Windows) is provided. ### Continuous Deployments -In addition to running tests, an additional deploy stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with Azure as the `Azure secrets` variable group. This variable group needs to be created only once per organization. For example: +In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: ``` -# create a resource group if none exists -az group create --name main --location eastus2 -# create the vault (try different names until something works) -az keyvault create --name secrets007 --resource-group main -# create the secret -az keyvault secret set --vault-name secrets007 --name PyPI-token --value $token +pip-run -q setuptools jaraco.develop -- -m jaraco.develop.add-github-secret PYPI_TOKEN $TOKEN --project org/repo ``` -Then, in the web UI for the project's Pipelines Library, create the `Azure secrets` variable group referencing the key vault name. - -For more details, see [this blog entry](https://blog.jaraco.com/configuring-azure-pipelines-with-secets/). + ## Building Documentation From 5474714ed2622af66674fafe62ae01180c0adf81 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2020 14:20:21 -0400 Subject: [PATCH 394/835] Use RTD v2 config --- .readthedocs.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 8ae44684..cc698548 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,6 @@ +version: 2 python: - version: 3 - extra_requirements: - - docs - pip_install: true + install: + - path: . + extra_requirements: + - docs From 6ad08b8489ac1c1eba37d32525fa7fa8465076c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2020 14:08:14 -0400 Subject: [PATCH 395/835] Test on Python 3.9. Skip 3.7 to avoid creating too many builds. Release on 3.9. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b3dd81fc..8c5c232c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,13 +6,13 @@ jobs: test: strategy: matrix: - python: [3.6, 3.7, 3.8] + python: [3.6, 3.8, 3.9] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install tox @@ -29,9 +29,9 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install tox run: | python -m pip install tox From ca9ad41aeb98be511d9451706a9e29dd5016df00 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Oct 2020 14:09:25 -0400 Subject: [PATCH 396/835] Drop tests on Travis, Appveyor, and Azure Pipelines. --- .travis.yml | 19 ------------ README.rst | 11 ++----- appveyor.yml | 24 --------------- azure-pipelines.yml | 72 --------------------------------------------- skeleton.md | 26 +--------------- 5 files changed, 4 insertions(+), 148 deletions(-) delete mode 100644 .travis.yml delete mode 100644 appveyor.yml delete mode 100644 azure-pipelines.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index eed7b0a4..00000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -dist: bionic -language: python - -python: -- 3.6 -- &latest_py3 3.8 - -cache: pip - -install: -# ensure virtualenv is upgraded to avoid issues like jaraco/path#188 -- pip install -U --upgrade-strategy=eager tox - -before_script: - # Enable IPv6. Ref travis-ci/travis-ci#8361 - - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then - sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; - fi -script: tox diff --git a/README.rst b/README.rst index 4c7fd554..69554ef8 100644 --- a/README.rst +++ b/README.rst @@ -6,18 +6,13 @@ .. _PyPI link: https://pypi.org/project/skeleton -.. image:: https://dev.azure.com/jaraco/skeleton/_apis/build/status/jaraco.skeleton?branchName=master - :target: https://dev.azure.com/jaraco/skeleton/_build/latest?definitionId=1&branchName=master - -.. image:: https://img.shields.io/travis/jaraco/skeleton/master.svg - :target: https://travis-ci.org/jaraco/skeleton +.. image:: https://github.com/jaraco/skeleton/workflows/Automated%20Tests/badge.svg + :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22Automated+Tests%22 + :alt: Automated Tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black -.. .. image:: https://img.shields.io/appveyor/ci/jaraco/skeleton/master.svg -.. :target: https://ci.appveyor.com/project/jaraco/skeleton/branch/master - .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index c6f46e4f..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,24 +0,0 @@ -environment: - - APPVEYOR: true - - matrix: - - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python38-x64" - -install: - # symlink python from a directory with a space - - "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" - - "SET PYTHON=\"C:\\Program Files\\Python\"" - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - -build: off - -cache: - - '%LOCALAPPDATA%\pip\Cache' - -test_script: - - "python -m pip install -U tox virtualenv" - - "tox" - -version: '{build}' diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6d318994..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,72 +0,0 @@ -# Create the project in Azure with: -# az devops project create --name $name --organization https://dev.azure.com/$org/ --visibility public -# then configure the pipelines (through web UI) - -trigger: - branches: - include: - - '*' - tags: - include: - - '*' - -pool: - vmImage: $(pool_vm_image) - -variables: -- group: Azure secrets -- name: pool_vm_image - value: Ubuntu-18.04 - -stages: -- stage: Test - jobs: - - - job: 'Test' - strategy: - matrix: - Bionic Python 3.6: - python.version: '3.6' - Bionic Python 3.8: - python.version: '3.8' - Windows Python 3.8: - python.version: '3.8' - pool_vm_image: vs2017-win2016 - Windows Python Prerelease: - python.version: '3.9' - pool_vm_image: vs2017-win2016 - MacOS: - python.version: '3.8' - pool_vm_image: macos-10.15 - - maxParallel: 4 - - steps: - - task: NuGetToolInstaller@1 - displayName: 'Install NuGet' - condition: eq(variables['pool_vm_image'], 'vs2017-win2016') - - - powershell: | - nuget install python -Prerelease -OutputDirectory "$(Build.BinariesDirectory)" -ExcludeVersion -NonInteractive - Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools" - Write-Host "##vso[task.prependpath]$(Build.BinariesDirectory)\python\tools\Scripts" - condition: and(succeeded(), and(eq(variables['python.version'], '3.9'), eq(variables['pool_vm_image'], 'vs2017-win2016'))) - - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - architecture: 'x64' - condition: and(succeeded(), ne(variables['python.version'], '3.9')) - - - script: python -m pip install tox - displayName: 'Install tox' - - - script: | - tox -- --junit-xml=test-results.xml - displayName: 'run tests' - - - task: PublishTestResults@2 - inputs: - testResultsFiles: '**/test-results.xml' - testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() diff --git a/skeleton.md b/skeleton.md index c6b0cd0c..7c3956c7 100644 --- a/skeleton.md +++ b/skeleton.md @@ -108,7 +108,7 @@ Relies on a .flake8 file to correct some default behaviors: ## Continuous Integration -The project is pre-configured to run tests through multiple CI providers. +The project is pre-configured to run Continuous Integration tests. ### Github Actions @@ -119,30 +119,6 @@ Features include: - run on late (and updated) platform versions - automated releases of tagged commits -### Azure Pipelines - -[Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) were adopted for free, fast, multi-platform services. See azure-pipelines.yml for more details. - -Azure Pipelines require many [complicated setup steps](https://github.com/Azure/azure-devops-cli-extension/issues/968) that have not been readily automated. - -Features include: - -- test against multiple Python versions -- run on Ubuntu Bionic - -### Travis CI - -[Travis CI](https://travis-ci.org) is configured through .travis.yml. Any new project must be enabled either through their web site or with the `travis enable` command. - -Features include: -- test against Python 3 -- run on Ubuntu Bionic -- correct for broken IPv6 - -### AppVeyor - -A minimal template for running under AppVeyor (Windows) is provided. - ### Continuous Deployments In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: From 1311cecaad5e176eb7604a045d16dcd6c7353a45 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Oct 2020 10:18:23 -0400 Subject: [PATCH 397/835] use add-github-secrets, which infers the secrets needed from the github workflow. --- skeleton.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/skeleton.md b/skeleton.md index 7c3956c7..ec421c25 100644 --- a/skeleton.md +++ b/skeleton.md @@ -124,11 +124,9 @@ Features include: In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: ``` -pip-run -q setuptools jaraco.develop -- -m jaraco.develop.add-github-secret PYPI_TOKEN $TOKEN --project org/repo +pip-run -q jaraco.develop -- -m jaraco.develop.add-github-secrets ``` - - ## Building Documentation Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. From 95ce6f33cc095df6d0a5f239e075a610eefbe262 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Oct 2020 20:58:20 -0400 Subject: [PATCH 398/835] Use inline flags with local scope. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 41b53557..433d185d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ url='{package_url}/issues/{issue}', ), dict( - pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( From fb8d9c43500c06f6f2286ea7c7ae452d41cce412 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 14 Nov 2020 20:32:18 -0500 Subject: [PATCH 399/835] Move Tidelift release note publishing to Github Actions. --- .github/workflows/main.yml | 6 ++++++ azure-pipelines.yml | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..01999cab --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,6 @@ +jobs: + release: + steps: + - name: Release + env: + TIDELIFT_TOKEN: ${{ secrets.TIDELIFT_TOKEN }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 01bfa5f5..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,2 +0,0 @@ - env: - TIDELIFT_TOKEN: $(Tidelift-token) From 27f7c53a66c077cea17896496330bab97f1db54b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 19 Nov 2020 22:02:41 -0500 Subject: [PATCH 400/835] Honor TOX_WORK_DIR if set. Workaround for tox-dev/tox#20. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 7347d95f..7233b942 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true +toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] From cfb3ce5472939f9c29c4561676d9fc66fb3bcb81 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Dec 2020 12:37:12 -0500 Subject: [PATCH 401/835] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2a785b72..81873b07 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v15.0.1 +------- + +- Refreshed package metadata. + v15.0.0 ------- From c681f6748acaea1bf0b706528c36327cc94a6eed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Dec 2020 16:29:09 -0500 Subject: [PATCH 402/835] Collapse skeleton history from archive/2020-12 --- .coveragerc | 5 ++ .flake8 | 9 +++ .github/workflows/main.yml | 42 +++++++++++ .pre-commit-config.yaml | 10 +++ .readthedocs.yml | 6 ++ CHANGES.rst | 0 LICENSE | 19 +++++ README.rst | 18 +++++ docs/conf.py | 26 +++++++ docs/history.rst | 8 +++ docs/index.rst | 22 ++++++ mypy.ini | 2 + pyproject.toml | 22 ++++++ pytest.ini | 9 +++ setup.cfg | 45 ++++++++++++ setup.py | 6 ++ skeleton.md | 144 +++++++++++++++++++++++++++++++++++++ tox.ini | 40 +++++++++++ 18 files changed, 433 insertions(+) create mode 100644 .coveragerc create mode 100644 .flake8 create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yml create mode 100644 CHANGES.rst create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 docs/conf.py create mode 100644 docs/history.rst create mode 100644 docs/index.rst create mode 100644 mypy.ini create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 skeleton.md create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..45823064 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = .tox/* + +[report] +show_missing = True diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..790c109f --- /dev/null +++ b/.flake8 @@ -0,0 +1,9 @@ +[flake8] +max-line-length = 88 +ignore = + # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 + W503 + # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 + W504 + # Black creates whitespace before colon + E203 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..8c5c232c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,42 @@ +name: Automated Tests + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: [3.6, 3.8, 3.9] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + release: + needs: test + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6639c78c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + +- repo: https://github.com/asottile/blacken-docs + rev: v1.8.0 + hooks: + - id: blacken-docs diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..cc698548 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,6 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 00000000..e69de29b diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..69554ef8 --- /dev/null +++ b/README.rst @@ -0,0 +1,18 @@ +.. image:: https://img.shields.io/pypi/v/skeleton.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/skeleton.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/skeleton + +.. image:: https://github.com/jaraco/skeleton/workflows/Automated%20Tests/badge.svg + :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22Automated+Tests%22 + :alt: Automated Tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest +.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..433d185d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] + +master_doc = "index" + +link_files = { + '../CHANGES.rst': dict( + using=dict(GH='https://github.com'), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', + ), + ], + ) +} diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..8e217503 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../CHANGES (links).rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..d14131b0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to skeleton documentation! +======================================== + +.. toctree:: + :maxdepth: 1 + + history + + +.. automodule:: skeleton + :members: + :undoc-members: + :show-inheritance: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..976ba029 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..79f088a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" + +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] + +# jaraco/skeleton#22 +[tool.jaraco.pytest.plugins.black] +addopts = "--black" + +# jaraco/skeleton#22 +[tool.jaraco.pytest.plugins.mypy] +addopts = "--mypy" + +[tool.jaraco.pytest.plugins.flake8] +addopts = "--flake8" + +[tool.jaraco.pytest.plugins.cov] +addopts = "--cov" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..d7f0b115 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +norecursedirs=dist build .tox .eggs +addopts=--doctest-modules +doctest_optionflags=ALLOW_UNICODE ELLIPSIS +# workaround for warning pytest-dev/pytest#6178 +junit_family=xunit2 +filterwarnings= + # https://github.com/pytest-dev/pytest/issues/6928 + ignore:direct construction of .*Item has been deprecated:DeprecationWarning diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..6321ca77 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[metadata] +license_file = LICENSE +name = skeleton +author = Jason R. Coombs +author_email = jaraco@jaraco.com +description = skeleton +long_description = file:README.rst +url = https://github.com/jaraco/skeleton +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + +[options] +packages = find: +include_package_data = true +python_requires = >=3.6 +install_requires = +setup_requires = setuptools_scm[toml] >= 3.4.1 + +[options.extras_require] +testing = + # upstream + pytest >= 3.5, !=3.7.3 + pytest-checkdocs >= 1.2.3 + pytest-flake8 + pytest-black >= 0.3.7; python_implementation != "PyPy" + pytest-cov + pytest-mypy; python_implementation != "PyPy" + # jaraco/skeleton#22 + jaraco.test >= 3.2.0 + + # local + +docs = + # upstream + sphinx + jaraco.packaging >= 3.2 + rst.linker >= 1.9 + + # local + +[options.entry_points] diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..bac24a43 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +import setuptools + +if __name__ == "__main__": + setuptools.setup() diff --git a/skeleton.md b/skeleton.md new file mode 100644 index 00000000..ec421c25 --- /dev/null +++ b/skeleton.md @@ -0,0 +1,144 @@ +# Overview + +This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. + +## An SCM-Managed Approach + +While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a Git repo capturing the evolution and culmination of these best practices. + +It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. + +The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. + +Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. + +# Usage + +## new projects + +To use skeleton for a new project, simply pull the skeleton into a new project: + +``` +$ git init my-new-project +$ cd my-new-project +$ git pull gh://jaraco/skeleton +``` + +Now customize the project to suit your individual project needs. + +## existing projects + +If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. + +``` +$ git merge skeleton --allow-unrelated-histories +``` + +The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. + +## Updating + +Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar Git operations. + +For example, here's a session of the [path project](https://pypi.org/project/path) pulling non-conflicting changes from the skeleton: + + + +Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. + +# Features + +The features/techniques employed by the skeleton include: + +- PEP 517/518-based build relying on Setuptools as the build tool +- Setuptools declarative configuration using setup.cfg +- tox for running tests +- A README.rst as reStructuredText with some popular badges, but with Read the Docs and AppVeyor badges commented out +- A CHANGES.rst file intended for publishing release notes about the project +- Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) +- Integrated type checking through [mypy](https://github.com/python/mypy/). + +## Packaging Conventions + +A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on Setuptools (a minimum version compatible with setup.cfg declarative config). + +The setup.cfg file implements the following features: + +- Assumes universal wheel for release +- Advertises the project's LICENSE file (MIT by default) +- Reads the README.rst file into the long description +- Some common Trove classifiers +- Includes all packages discovered in the repo +- Data files in the package are also included (not just Python files) +- Declares the required Python versions +- Declares install requirements (empty by default) +- Declares setup requirements for legacy environments +- Supplies two 'extras': + - testing: requirements for running tests + - docs: requirements for building docs + - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts +- Placeholder for defining entry points + +Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: + +- derive the project version from SCM tags +- ensure that all files committed to the repo are automatically included in releases + +## Running Tests + +The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). + +Other environments (invoked with `tox -e {name}`) supplied include: + + - a `docs` environment to build the documentation + - a `release` environment to publish the package to PyPI + +A pytest.ini is included to define common options around running tests. In particular: + +- rely on default test discovery in the current directory +- avoid recursing into common directories not containing tests +- run doctests on modules and invoke Flake8 tests +- in doctests, allow Unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. +- filters out known warnings caused by libraries/functionality included by the skeleton + +Relies on a .flake8 file to correct some default behaviors: + +- disable mutually incompatible rules W503 and W504 +- support for Black format + +## Continuous Integration + +The project is pre-configured to run Continuous Integration tests. + +### Github Actions + +[Github Actions](https://docs.github.com/en/free-pro-team@latest/actions) are the preferred provider as they provide free, fast, multi-platform services with straightforward configuration. Configured in `.github/workflows`. + +Features include: +- test against multiple Python versions +- run on late (and updated) platform versions +- automated releases of tagged commits + +### Continuous Deployments + +In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: + +``` +pip-run -q jaraco.develop -- -m jaraco.develop.add-github-secrets +``` + +## Building Documentation + +Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. + +In addition to building the Sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. + +## Cutting releases + +By default, tagged commits are released through the continuous integration deploy stage. + +Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: + +``` +TWINE_PASSWORD={token} tox -e release +``` diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..7233b942 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +envlist = python +minversion = 3.2 +# https://github.com/jaraco/skeleton/issues/6 +tox_pip_extensions_ext_venv_update = true +toxworkdir={env:TOX_WORK_DIR:.tox} + + +[testenv] +deps = +commands = + pytest {posargs} +usedevelop = True +extras = testing + +[testenv:docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx . {toxinidir}/build/html + +[testenv:release] +skip_install = True +deps = + pep517>=0.5 + twine[keyring]>=1.13 + path + jaraco.develop>=7.1 +passenv = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import path; path.Path('dist').rmtree_p()" + python -m pep517.build . + python -m twine upload dist/* + python -m jaraco.develop.create-github-release From 2667241f44fed464948cbd140bed1b17cfe4e826 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Dec 2020 23:29:03 -0500 Subject: [PATCH 403/835] Update skeleton description to describe the periodic collapse. Fixes #27. --- skeleton.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/skeleton.md b/skeleton.md index ec421c25..dd8ec014 100644 --- a/skeleton.md +++ b/skeleton.md @@ -46,6 +46,26 @@ For example, here's a session of the [path project](https://pypi.org/project/pat Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. +## Periodic Collapse + +In late 2020, this project [introduced](https://github.com/jaraco/skeleton/issues/27) the idea of a periodic but infrequent (O(years)) collapse of commits to limit the number of commits a new consumer will need to accept to adopt the skeleton. + +The full history of commits is collapsed into a single commit and that commit becomes the new mainline head. + +When one of these collapse operations happens, any project that previously pulled from the skeleton will no longer have a related history with that new main branch. For those projects, the skeleton provides a "handoff" branch that reconciles the two branches. Any project that has previously merged with the skeleton but now gets an error "fatal: refusing to merge unrelated histories" should instead use the handoff branch once to incorporate the new main branch. + +``` +$ git pull https://github.com/jaraco/skeleton 2020-handoff +``` + +This handoff needs to be pulled just once and thereafter the project can pull from the main head. + +The archive and handoff branches from prior collapses are indicate here: + +| refresh | archive | handoff | +|---------|-----------------|--------------| +| 2020-12 | archive/2020-12 | 2020-handoff | + # Features The features/techniques employed by the skeleton include: From 150321caba0dc73489b61d6b5bbfbed52b795ae7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 13 Dec 2020 14:03:23 -0500 Subject: [PATCH 404/835] Enable automerge --- .github/workflows/automerge.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/automerge.yml diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 00000000..4f70acfb --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,27 @@ +name: automerge +on: + pull_request: + types: + - labeled + - unlabeled + - synchronize + - opened + - edited + - ready_for_review + - reopened + - unlocked + pull_request_review: + types: + - submitted + check_suite: + types: + - completed + status: {} +jobs: + automerge: + runs-on: ubuntu-latest + steps: + - name: automerge + uses: "pascalgn/automerge-action@v0.12.0" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From 4b1334629e1cb254a1b6853f045f2615b79ec9e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 29 Dec 2020 09:56:52 -0500 Subject: [PATCH 405/835] Automatically inject project name in docs heading. --- docs/index.rst | 4 ++-- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index d14131b0..325842bb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ -Welcome to skeleton documentation! -======================================== +Welcome to |project| documentation! +=================================== .. toctree:: :maxdepth: 1 diff --git a/setup.cfg b/setup.cfg index 6321ca77..4fc095b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ testing = docs = # upstream sphinx - jaraco.packaging >= 3.2 + jaraco.packaging >= 8.2 rst.linker >= 1.9 # local From cfe99a5a7941f9f8785dd3ec12d1df94e9134411 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 29 Dec 2020 21:27:53 -0500 Subject: [PATCH 406/835] pre-commit autoupdate --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6639c78c..c15ab0c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: stable + rev: 20.8b1 hooks: - id: black - repo: https://github.com/asottile/blacken-docs - rev: v1.8.0 + rev: v1.9.1 hooks: - id: blacken-docs From 060d491a9aaacfe457ad365cfd60b611fc9f5bcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Dec 2020 10:57:25 -0500 Subject: [PATCH 407/835] Rename 'Automated Tests' to simply 'tests' --- .github/workflows/main.yml | 2 +- README.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c5c232c..6a8ff006 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Automated Tests +name: tests on: [push, pull_request] diff --git a/README.rst b/README.rst index 69554ef8..128e61e4 100644 --- a/README.rst +++ b/README.rst @@ -6,9 +6,9 @@ .. _PyPI link: https://pypi.org/project/skeleton -.. image:: https://github.com/jaraco/skeleton/workflows/Automated%20Tests/badge.svg - :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22Automated+Tests%22 - :alt: Automated Tests +.. image:: https://github.com/jaraco/skeleton/workflows/tests/badge.svg + :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22tests%22 + :alt: tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black From 2b839bad1c2189f4eeb0f74c4a2455ba6687741b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Dec 2020 12:06:13 -0500 Subject: [PATCH 408/835] Add note about automatic merging of PRs and the requirements and limitations. --- skeleton.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skeleton.md b/skeleton.md index dd8ec014..0938f892 100644 --- a/skeleton.md +++ b/skeleton.md @@ -138,6 +138,8 @@ Features include: - test against multiple Python versions - run on late (and updated) platform versions - automated releases of tagged commits +- [automatic merging of PRs](https://github.com/marketplace/actions/merge-pull-requests) (requires [protecting branches with required status checks](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/enabling-required-status-checks), [not possible through API](https://github.community/t/set-all-status-checks-to-be-required-as-branch-protection-using-the-github-api/119493)) + ### Continuous Deployments From a36768aa363c8f7b54aae00e11f895ff06337532 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Dec 2020 22:20:46 -0500 Subject: [PATCH 409/835] Prefer pytest-enabler to jaraco.test --- pyproject.toml | 10 ++++------ setup.cfg | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 79f088a9..b6ebc0be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,16 +7,14 @@ skip-string-normalization = true [tool.setuptools_scm] -# jaraco/skeleton#22 -[tool.jaraco.pytest.plugins.black] +[pytest.enabler.black] addopts = "--black" -# jaraco/skeleton#22 -[tool.jaraco.pytest.plugins.mypy] +[pytest.enabler.mypy] addopts = "--mypy" -[tool.jaraco.pytest.plugins.flake8] +[pytest.enabler.flake8] addopts = "--flake8" -[tool.jaraco.pytest.plugins.cov] +[pytest.enabler.cov] addopts = "--cov" diff --git a/setup.cfg b/setup.cfg index 4fc095b3..d5010f70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,8 +29,7 @@ testing = pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov pytest-mypy; python_implementation != "PyPy" - # jaraco/skeleton#22 - jaraco.test >= 3.2.0 + pytest-enabler # local From 3e876d7906fa6387ab6ac9a9bff8659762363017 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 Jan 2021 23:14:07 -0500 Subject: [PATCH 410/835] Enable complexity limit. Fixes jaraco/skeleton#34. --- .flake8 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.flake8 b/.flake8 index 790c109f..59a51f86 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,9 @@ [flake8] max-line-length = 88 + +# jaraco/skeleton#34 +max-complexity = 10 + ignore = # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 W503 From 1731fbebe9f6655a203e6e08ab309f9916ea6f65 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 9 Jan 2021 05:21:12 +0100 Subject: [PATCH 411/835] Replace pep517.build with build (#37) * Replace pep517.build with build Resolves #30 * Prefer simple usage Co-authored-by: Jason R. Coombs --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 7233b942..249f97c2 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ commands = [testenv:release] skip_install = True deps = - pep517>=0.5 + build twine[keyring]>=1.13 path jaraco.develop>=7.1 @@ -35,6 +35,6 @@ setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import path; path.Path('dist').rmtree_p()" - python -m pep517.build . + python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release From a9b3f681dea9728235c2a9c68165f7b5cbf350ab Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 9 Jan 2021 05:27:24 +0100 Subject: [PATCH 412/835] Use license_files instead of license_file in meta (#35) Singular `license_file` is deprecated since wheel v0.32.0. Refs: * https://wheel.readthedocs.io/en/stable/news.html * https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d5010f70..88bc263a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] -license_file = LICENSE +license_files = + LICENSE name = skeleton author = Jason R. Coombs author_email = jaraco@jaraco.com From 77fbe1df4af6d8f75f44440e89ee1bc249c9f2e0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 9 Jan 2021 05:37:11 +0100 Subject: [PATCH 413/835] Use `extend-ignore` in flake8 config (#33) * Use `extend-ignore` in flake8 config This option allows to add extra ignored rules to the default list instead of replacing it. The default exclusions are: E121, E123, E126, E226, E24, E704, W503 and W504. Fixes #28. Refs: * https://github.com/pypa/setuptools/pull/2486/files#r541943356 * https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-extend-ignore * https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore * Enable complexity limit. Fixes jaraco/skeleton#34. * Replace pep517.build with build (#37) * Replace pep517.build with build Resolves #30 * Prefer simple usage Co-authored-by: Jason R. Coombs * Use license_files instead of license_file in meta (#35) Singular `license_file` is deprecated since wheel v0.32.0. Refs: * https://wheel.readthedocs.io/en/stable/news.html * https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file Co-authored-by: Jason R. Coombs --- .flake8 | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.flake8 b/.flake8 index 59a51f86..48b2e246 100644 --- a/.flake8 +++ b/.flake8 @@ -4,10 +4,6 @@ max-line-length = 88 # jaraco/skeleton#34 max-complexity = 10 -ignore = - # W503 violates spec https://github.com/PyCQA/pycodestyle/issues/513 - W503 - # W504 has issues https://github.com/OCA/maintainer-quality-tools/issues/545 - W504 +extend-ignore = # Black creates whitespace before colon E203 From e3cf890bbff2c68cd381aa08ce26f508e127029c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:01:12 -0500 Subject: [PATCH 414/835] Extract Handlers class. Fixes complexity. --- path/__init__.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 12e63ee0..1fd5a6cd 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -554,20 +554,7 @@ def walk(self, match=None, errors='strict'): `errors` may also be an arbitrary callable taking a msg parameter. """ - class Handlers: - def strict(msg): - raise - - def warn(msg): - warnings.warn(msg, TreeWalkWarning) - - def ignore(msg): - pass - - if not callable(errors) and errors not in vars(Handlers): - raise ValueError("invalid errors parameter") - errors = vars(Handlers).get(errors, errors) - + errors = Handlers._resolve(errors) match = matchers.load(match) try: @@ -1709,3 +1696,20 @@ def _permission_mask(mode): '=': lambda mask, target: target & retain ^ mask, } return functools.partial(op_map[op], mask) + + +class Handlers: + def strict(msg): + raise + + def warn(msg): + warnings.warn(msg, TreeWalkWarning) + + def ignore(msg): + pass + + @classmethod + def _resolve(cls, param): + if not callable(param) and param not in vars(Handlers): + raise ValueError("invalid errors parameter") + return vars(cls).get(param, param) From a149a7bca78ca6d8bb635bed380b786e6723b364 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:11:32 -0500 Subject: [PATCH 415/835] Simplify in_place --- path/__init__.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1fd5a6cd..bf862719 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1403,11 +1403,8 @@ def in_place( # move existing file to backup, create new file with same permissions # borrowed extensively from the fileinput module backup_fn = self + (backup_extension or os.extsep + 'bak') - try: - os.unlink(backup_fn) - except os.error: - pass - os.rename(self, backup_fn) + backup_fn.remove_p() + self.rename(backup_fn) readable = io.open( backup_fn, mode, @@ -1419,8 +1416,7 @@ def in_place( try: perm = os.fstat(readable.fileno()).st_mode except OSError: - writable = open( - self, + writable = self.open( 'w' + mode.replace('r', ''), buffering=buffering, encoding=encoding, @@ -1440,31 +1436,22 @@ def in_place( errors=errors, newline=newline, ) - try: - if hasattr(os, 'chmod'): - os.chmod(self, perm) - except OSError: - pass + with contextlib.suppress(OSError, AttributeError): + self.chmod(perm) try: yield readable, writable except Exception: # move backup back readable.close() writable.close() - try: - os.unlink(self) - except os.error: - pass - os.rename(backup_fn, self) + self.remove_p() + backup_fn.rename(self) raise else: readable.close() writable.close() finally: - try: - os.unlink(backup_fn) - except os.error: - pass + backup_fn.remove_p() @ClassProperty @classmethod From fca8c2914cba5efceccbffde626239df115e05a2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:15:09 -0500 Subject: [PATCH 416/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/py37compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/py37compat.py b/path/py37compat.py index 0a769def..f2a9e8b4 100644 --- a/path/py37compat.py +++ b/path/py37compat.py @@ -39,7 +39,7 @@ def realpath_backport(path): return path -def _resolve_path(path, rest, seen): +def _resolve_path(path, rest, seen): # noqa: C901 # Windows normalizes the path before resolving symlinks; be sure to # follow the same behavior. rest = os.path.normpath(rest) From ac05242443d9dea07e61b0b2045eb4eeaa13efbe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:16:16 -0500 Subject: [PATCH 417/835] Extract encodings variable --- test_path.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index 6ed5d97e..68e323ed 100644 --- a/test_path.py +++ b/test_path.py @@ -582,7 +582,9 @@ def test_patterns(self, tmpdir): self.assertList(d.walkfiles('*.tmp'), [e / 'x.tmp' for e in dirs]) self.assertList(d.walkdirs('*.tmp'), [d / 'xdir.tmp']) - @pytest.mark.parametrize("encoding", ('UTF-8', 'UTF-16BE', 'UTF-16LE', 'UTF-16')) + encodings = 'UTF-8', 'UTF-16BE', 'UTF-16LE', 'UTF-16' + + @pytest.mark.parametrize("encoding", encodings) def test_unicode(self, tmpdir, encoding): """Test that path works with the specified encoding, which must be capable of representing the entire range of From 1239abd2ada929dd34cd371730e2b306c7541ba8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:17:53 -0500 Subject: [PATCH 418/835] Remove legacy skip marker --- test_path.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test_path.py b/test_path.py index 68e323ed..909f945c 100644 --- a/test_path.py +++ b/test_path.py @@ -1040,9 +1040,6 @@ def test_walk_case_insensitive(self, tmpdir): assert p / 'sub1' / 'foo' / 'bar.Txt' in files -@pytest.mark.skipif( - sys.version_info < (2, 6), reason="in_place requires io module in Python 2.6" -) class TestInPlace: reference_content = textwrap.dedent( """ From e9d1e9dd034cd1651c6846d00aa828c7ac9438be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:22:32 -0500 Subject: [PATCH 419/835] Prefer 'os_choose' to 'p' --- test_path.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test_path.py b/test_path.py index 909f945c..e7233880 100644 --- a/test_path.py +++ b/test_path.py @@ -40,7 +40,7 @@ from path import Multi -def p(**choices): +def os_choose(**choices): """ Choose a value from several possible values, based on os.name """ return choices[os.name] @@ -56,7 +56,7 @@ def mac_version(target, comparator=operator.ge): class TestBasics: def test_relpath(self): - root = Path(p(nt='C:\\', posix='/')) + root = Path(os_choose(nt='C:\\', posix='/')) foo = root / 'foo' quux = foo / 'quux' bar = foo / 'bar' @@ -117,32 +117,33 @@ def test_string_compatibility(self): # Test p1/p1. p1 = Path("foo") p2 = Path("bar") - assert p1 / p2 == p(nt='foo\\bar', posix='foo/bar') + assert p1 / p2 == os_choose(nt='foo\\bar', posix='foo/bar') def test_properties(self): # Create sample path object. - f = p( - nt='C:\\Program Files\\Python\\Lib\\xyzzy.py', - posix='/usr/local/python/lib/xyzzy.py', + f = Path( + os_choose( + nt='C:\\Program Files\\Python\\Lib\\xyzzy.py', + posix='/usr/local/python/lib/xyzzy.py', + ) ) - f = Path(f) # .parent nt_lib = 'C:\\Program Files\\Python\\Lib' posix_lib = '/usr/local/python/lib' - expected = p(nt=nt_lib, posix=posix_lib) + expected = os_choose(nt=nt_lib, posix=posix_lib) assert f.parent == expected # .name assert f.name == 'xyzzy.py' - assert f.parent.name == p(nt='Lib', posix='lib') + assert f.parent.name == os_choose(nt='Lib', posix='lib') # .ext assert f.ext == '.py' assert f.parent.ext == '' # .drive - assert f.drive == p(nt='C:', posix='') + assert f.drive == os_choose(nt='C:', posix='') def test_methods(self): # .abspath() @@ -185,7 +186,7 @@ def test_explicit_module_classes(self): def test_joinpath_on_instance(self): res = Path('foo') foo_bar = res.joinpath('bar') - assert foo_bar == p(nt='foo\\bar', posix='foo/bar') + assert foo_bar == os_choose(nt='foo\\bar', posix='foo/bar') def test_joinpath_to_nothing(self): res = Path('foo') @@ -194,7 +195,7 @@ def test_joinpath_to_nothing(self): def test_joinpath_on_class(self): "Construct a path from a series of strings" foo_bar = Path.joinpath('foo', 'bar') - assert foo_bar == p(nt='foo\\bar', posix='foo/bar') + assert foo_bar == os_choose(nt='foo\\bar', posix='foo/bar') def test_joinpath_fails_on_empty(self): "It doesn't make sense to join nothing at all" From cefcc04f1fe65e4e5e96800a48319056c43c5f2d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:36:00 -0500 Subject: [PATCH 420/835] Prefer suppress to except/pass --- path/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index bf862719..1e1915ad 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -39,20 +39,14 @@ import importlib import itertools -try: +with contextlib.suppress(ImportError): import win32security -except ImportError: - pass -try: +with contextlib.suppress(ImportError): import pwd -except ImportError: - pass -try: +with contextlib.suppress(ImportError): import grp -except ImportError: - pass from . import matchers from .py37compat import best_realpath, lru_cache From 230fe6d323f8d74fed80d3c1119676afb3dbca74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:41:08 -0500 Subject: [PATCH 421/835] Remove legacy exception handling. --- path/__init__.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1e1915ad..528fcb7c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -553,11 +553,8 @@ def walk(self, match=None, errors='strict'): try: childList = self.listdir() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to list directory '%(self)s': %(exc)s" - msg = tmpl % locals() - errors(msg) + except Exception as exc: + errors(f"Unable to list directory '{self}': {exc}") return for child in childList: @@ -567,12 +564,8 @@ def walk(self, match=None, errors='strict'): traverse = traverse or child.isdir try: do_traverse = traverse() - except Exception: - exc = sys.exc_info()[1] - tmpl = "Unable to access '%(child)s': %(exc)s" - msg = tmpl % locals() - errors(msg) - isdir = False + except Exception as exc: + errors(f"Unable to access '{child}': {exc}") if do_traverse: for item in child.walk(errors=errors, match=match): @@ -1295,7 +1288,7 @@ def merge_tree( symlinks=False, *, copy_function=shutil.copy2, - ignore=lambda dir, contents: [] + ignore=lambda dir, contents: [], ): """ Copy entire contents of self to dst, overwriting existing From a0207ca74ab0b1367a97f3f024c577768adc9c85 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:44:41 -0500 Subject: [PATCH 422/835] Remove mac_version, unused --- test_path.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test_path.py b/test_path.py index e7233880..f2957f97 100644 --- a/test_path.py +++ b/test_path.py @@ -24,13 +24,11 @@ import textwrap import platform import importlib -import operator import datetime import subprocess import re import pytest -import packaging.version import path from path import Path @@ -45,15 +43,6 @@ def os_choose(**choices): return choices[os.name] -def mac_version(target, comparator=operator.ge): - """ - Return True if on a Mac whose version passes the comparator. - """ - current_ver = packaging.version.parse(platform.mac_ver()[0]) - target_ver = packaging.version.parse(target) - return platform.system() == 'Darwin' and comparator(current_ver, target_ver) - - class TestBasics: def test_relpath(self): root = Path(os_choose(nt='C:\\', posix='/')) From 594e0e311090746376857d7234631c3e5bb276de Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:45:56 -0500 Subject: [PATCH 423/835] Prefer pytest.raises --- test_path.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test_path.py b/test_path.py index f2957f97..1e9c7d64 100644 --- a/test_path.py +++ b/test_path.py @@ -79,12 +79,8 @@ def test_relpath(self): def test_construction_from_none(self): """""" - try: + with pytest.raises(TypeError): Path(None) - except TypeError: - pass - else: - raise Exception("DID NOT RAISE") def test_construction_from_int(self): """ From 7b22ce1374569d216846d9e03a89097625411bd5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:47:37 -0500 Subject: [PATCH 424/835] Rely on pytest to skip irrelevant test --- test_path.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_path.py b/test_path.py index 1e9c7d64..b0b67416 100644 --- a/test_path.py +++ b/test_path.py @@ -139,11 +139,11 @@ def test_methods(self): assert isinstance(cwd, Path) assert cwd == os.getcwd() + @pytest.mark.skipif('not hasattr(os.path, "splitunc")') def test_UNC(self): - if hasattr(os.path, 'splitunc'): - p = Path(r'\\python1\share1\dir1\file1.txt') - assert p.uncshare == r'\\python1\share1' - assert p.splitunc() == os.path.splitunc(str(p)) + p = Path(r'\\python1\share1\dir1\file1.txt') + assert p.uncshare == r'\\python1\share1' + assert p.splitunc() == os.path.splitunc(str(p)) def test_explicit_module(self): """ From 3d6ca150fc73c02e14efd7ccc371099066e64b12 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:48:30 -0500 Subject: [PATCH 425/835] Prefer pytest.raises --- test_path.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test_path.py b/test_path.py index b0b67416..cdb6a114 100644 --- a/test_path.py +++ b/test_path.py @@ -184,12 +184,8 @@ def test_joinpath_on_class(self): def test_joinpath_fails_on_empty(self): "It doesn't make sense to join nothing at all" - try: + with pytest.raises(TypeError): Path.joinpath() - except TypeError: - pass - else: - raise Exception("did not raise") def test_joinpath_returns_same_type(self): path_posix = Path.using_module(posixpath) From cbd5dd577bc30027bd675062f44dd6e182f3c024 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:50:25 -0500 Subject: [PATCH 426/835] Use contextlib to suppress --- test_path.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test_path.py b/test_path.py index cdb6a114..bbb19a70 100644 --- a/test_path.py +++ b/test_path.py @@ -27,6 +27,7 @@ import datetime import subprocess import re +import contextlib import pytest @@ -392,10 +393,8 @@ def test_listing(self, tmpdir): assert files == files2 finally: for f in files: - try: + with contextlib.suppress(Exception): f.remove() - except Exception: - pass @pytest.mark.skipif( platform.system() != "Linux", From 95d3ce871a7bbccc75a65d1e0a7aa4fe3a3b0891 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jan 2021 01:53:50 -0500 Subject: [PATCH 427/835] Make separate test for rmtree_p_nonexistent --- test_path.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test_path.py b/test_path.py index bbb19a70..a6f1737d 100644 --- a/test_path.py +++ b/test_path.py @@ -703,13 +703,12 @@ def test_rmtree_p(self, tmpdir): (sub / 'afile').write_text('something') sub.rmtree_p() assert not sub.exists() - try: - sub.rmtree_p() - except OSError: - self.fail( - "Calling `rmtree_p` on non-existent directory " - "should not raise an exception." - ) + + def test_rmtree_p_nonexistent(self, tmpdir): + d = Path(tmpdir) + sub = d / 'subfolder' + assert not sub.exists() + sub.rmtree_p() def test_rmdir_p_exists(self, tmpdir): """ From 0df40810ec54590c888ae0e4073d73f731c91f4a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 15 Jan 2021 19:16:28 -0500 Subject: [PATCH 428/835] Add support for namespace packages. Closes jaraco/skeleton#40. --- setup.cfg | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 88bc263a..106763e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,12 +15,18 @@ classifiers = Programming Language :: Python :: 3 :: Only [options] -packages = find: +packages = find_namespace: include_package_data = true python_requires = >=3.6 install_requires = setup_requires = setuptools_scm[toml] >= 3.4.1 +[options.packages.find] +exclude = + build* + docs* + tests* + [options.extras_require] testing = # upstream From 6d0f1961c27a55ed31cce42eef1f10fbc3f3d4ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 28 Jan 2021 17:20:04 -0500 Subject: [PATCH 429/835] Add ExtantPath and ExtantFile --- CHANGES.rst | 6 ++++++ path/__init__.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 81873b07..11a33deb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v15.1.0 +------- + +- Added ``ExtantPath`` and ``ExtantFile`` objects that raise + errors when they reference a non-existent path or file. + v15.0.1 ------- diff --git a/path/__init__.py b/path/__init__.py index 528fcb7c..ead784ee 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -156,6 +156,8 @@ class Path(str): def __init__(self, other=''): if other is None: raise TypeError("Invalid initial value for path: None") + with contextlib.suppress(AttributeError): + self._validate() @classmethod @lru_cache @@ -1494,6 +1496,35 @@ def wrapper(src, dst, *args, **kwargs): return wrapper +class ExtantPath(Path): + """ + >>> ExtantPath('.') + ExtantPath('.') + >>> ExtantPath('does-not-exist') + Traceback (most recent call last): + OSError: does-not-exist does not exist. + """ + + def _validate(self): + if not self.exists(): + raise OSError(f"{self} does not exist.") + + +class ExtantFile(Path): + """ + >>> ExtantFile('.') + Traceback (most recent call last): + FileNotFoundError: . does not exist as a file. + >>> ExtantFile('does-not-exist') + Traceback (most recent call last): + FileNotFoundError: does-not-exist does not exist as a file. + """ + + def _validate(self): + if not self.isfile(): + raise FileNotFoundError(f"{self} does not exist as a file.") + + class SpecialResolver: class ResolverScope: def __init__(self, paths, scope): From 51298a2cc4faa7253e9fe41d7a9574cf9aac997c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Feb 2021 23:08:58 -0500 Subject: [PATCH 430/835] Normalize indentation --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 106763e3..8df8d273 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,9 @@ setup_requires = setuptools_scm[toml] >= 3.4.1 [options.packages.find] exclude = - build* - docs* - tests* + build* + docs* + tests* [options.extras_require] testing = From 743af7249d56e55a7c2c5f3111958ceee008d8ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 13:04:46 -0500 Subject: [PATCH 431/835] Exclude dist from discovered packages. Fixes jaraco/skeleton#46. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 8df8d273..af246415 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ setup_requires = setuptools_scm[toml] >= 3.4.1 [options.packages.find] exclude = build* + dist* docs* tests* From 28fc5b2b2483f357a1a10a95d60e8a2456812c74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 13:13:32 -0500 Subject: [PATCH 432/835] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 11a33deb..ded57d95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v15.1.1 +------- + +- Refreshed package metadata. + v15.1.0 ------- From 38fff62edb5e282f144dc77cc1bf5555367336d9 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Sat, 6 Feb 2021 23:03:13 +0300 Subject: [PATCH 433/835] Added an .editorconfig. Pull request jaraco/skeleton#43. --- .editorconfig | 15 +++++++++++++++ pytest.ini | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6385b573 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/pytest.ini b/pytest.ini index d7f0b115..016063b5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,5 +5,5 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 filterwarnings= - # https://github.com/pytest-dev/pytest/issues/6928 - ignore:direct construction of .*Item has been deprecated:DeprecationWarning + # https://github.com/pytest-dev/pytest/issues/6928 + ignore:direct construction of .*Item has been deprecated:DeprecationWarning From cd07c3fdca7f1e9541225bd0f42a51ea1c5238de Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 15:07:12 -0500 Subject: [PATCH 434/835] Fix links for Tidelift. CLoses #199. --- CHANGES.rst | 5 +++++ README.rst | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ded57d95..51011721 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v15.1.2 +------- + +- #199: Fixed broken link in README. + v15.1.1 ------- diff --git a/README.rst b/README.rst index fe197499..7f49f7b0 100644 --- a/README.rst +++ b/README.rst @@ -143,7 +143,7 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. -`Learn more `_. +`Learn more `_. Security Contact ================ From 5e416793c008c5ef285c37828072fbea5ced6d08 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 21:34:35 -0500 Subject: [PATCH 435/835] It's no longer necessary to filter this warning and it's not a warning anymore. --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 016063b5..6bf69af1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,5 +5,3 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 filterwarnings= - # https://github.com/pytest-dev/pytest/issues/6928 - ignore:direct construction of .*Item has been deprecated:DeprecationWarning From d9a13c77ce2a3efea70c97d219ca4335c0f03c40 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 21:36:53 -0500 Subject: [PATCH 436/835] Bump minimum pytest --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index af246415..81f70eea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ exclude = [options.extras_require] testing = # upstream - pytest >= 3.5, !=3.7.3 + pytest >= 4.6 pytest-checkdocs >= 1.2.3 pytest-flake8 pytest-black >= 0.3.7; python_implementation != "PyPy" From bf9fae2c0df316dc837d56ae68880620733d5ff6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Mar 2021 09:57:43 -0500 Subject: [PATCH 437/835] Require twine 3 with keyring unconditionally required. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 249f97c2..a9a50b01 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = skip_install = True deps = build - twine[keyring]>=1.13 + twine>=3 path jaraco.develop>=7.1 passenv = From 7bdab57872da46ef6a5a7f5ea9099a197bdc3131 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Mar 2021 12:23:48 -0500 Subject: [PATCH 438/835] Add comments indicating why the exclusions are present --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 81f70eea..dd215c65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,10 @@ testing = pytest >= 4.6 pytest-checkdocs >= 1.2.3 pytest-flake8 + # python_implementation: workaround for jaraco/skeleton#22 pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov + # python_implementation: workaround for jaraco/skeleton#22 pytest-mypy; python_implementation != "PyPy" pytest-enabler From 14312a5bd75d3313ffd3e14fc7fbbc2a9b05cee5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Mar 2021 12:24:21 -0500 Subject: [PATCH 439/835] Exclude mypy on Python 3.10 as workaround for python/typed_ast#156. --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index dd215c65..55497f8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,8 @@ testing = pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov # python_implementation: workaround for jaraco/skeleton#22 - pytest-mypy; python_implementation != "PyPy" + # python_version: workaround for python/typed_ast#156 + pytest-mypy; python_implementation != "PyPy" and python_version < "3.10" pytest-enabler # local From af5445115af0cb68e671a678538a0207389586be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Mar 2021 12:30:25 -0500 Subject: [PATCH 440/835] Bump minimums on pytest-checkdocs and pytest-enabler as found on Setuptools. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 55497f8e..3f6610be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ exclude = testing = # upstream pytest >= 4.6 - pytest-checkdocs >= 1.2.3 + pytest-checkdocs >= 2.4 pytest-flake8 # python_implementation: workaround for jaraco/skeleton#22 pytest-black >= 0.3.7; python_implementation != "PyPy" @@ -40,7 +40,7 @@ testing = # python_implementation: workaround for jaraco/skeleton#22 # python_version: workaround for python/typed_ast#156 pytest-mypy; python_implementation != "PyPy" and python_version < "3.10" - pytest-enabler + pytest-enabler >= 1.0.1 # local From 86efb884f805a9e1f64661ec758f3bd084fed515 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Mar 2021 12:53:54 -0500 Subject: [PATCH 441/835] Also deny black on Python 3.10 as workaround for python/typed_ast#156. --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3f6610be..52876d55 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,8 @@ testing = pytest-checkdocs >= 2.4 pytest-flake8 # python_implementation: workaround for jaraco/skeleton#22 - pytest-black >= 0.3.7; python_implementation != "PyPy" + # python_version: workaround for python/typed_ast#156 + pytest-black >= 0.3.7; python_implementation != "PyPy" and python_version < "3.10" pytest-cov # python_implementation: workaround for jaraco/skeleton#22 # python_version: workaround for python/typed_ast#156 From 7fe4ab8294a843622d20face7f9f6ccddb2d0a14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Mar 2021 18:31:04 -0400 Subject: [PATCH 442/835] Add leading */ to coverage.run.omit. Workaround for pytest-dev/pytest-cov#456. --- .coveragerc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 45823064..6a34e662 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,7 @@ [run] -omit = .tox/* +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* [report] show_missing = True From 4b1568c71b5299b39a5579bb778c8930991448b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 22 Mar 2021 19:58:47 -0400 Subject: [PATCH 443/835] Tidelift no longer requires or expects publishing release notes. --- tox.ini | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 35053514..00000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[testenv:release] -deps = - jaraco.tidelift -passenv = - TIDELIFT_TOKEN -commands = - python -m jaraco.tidelift.publish-release-notes From 842eb1423ba76dadbf568f9b0abf04a233711529 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Mar 2021 18:52:10 -0400 Subject: [PATCH 444/835] Remove Tidelift from main.yml, no longer needed --- .github/workflows/main.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 01999cab..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -jobs: - release: - steps: - - name: Release - env: - TIDELIFT_TOKEN: ${{ secrets.TIDELIFT_TOKEN }} From d6d4fee4d8eaa67148ba09146a8a25859c65ad74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Mar 2021 21:26:48 -0400 Subject: [PATCH 445/835] Add doctest for splitall --- path/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index ead784ee..90cc40cd 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -434,7 +434,10 @@ def splitall(self): directory of this path (for example, ``'/'`` or ``'C:\\'``). The other items in the list will be strings. - ``path.Path.joinpath(*result)`` will yield the original path. + ``Path.joinpath(*result)`` will yield the original path. + + >>> Path('/foo/bar/baz').splitall() + [Path('/'), 'foo', 'bar', 'baz'] """ parts = [] loc = self From b23ad440bece696572056ccc58c0118acd6ca534 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Mar 2021 21:32:01 -0400 Subject: [PATCH 446/835] Extract parts iteration from execution. Add 'parts' method for compatibility with pathlib. --- path/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 90cc40cd..8336d2b1 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -439,17 +439,23 @@ def splitall(self): >>> Path('/foo/bar/baz').splitall() [Path('/'), 'foo', 'bar', 'baz'] """ - parts = [] + return list(self._parts()) + + def parts(self): + return tuple(self._parts()) + + def _parts(self): + return reversed(tuple(self._parts_iter())) + + def _parts_iter(self): loc = self while loc != os.curdir and loc != os.pardir: prev = loc loc, child = prev.splitpath() if loc == prev: break - parts.append(child) - parts.append(loc) - parts.reverse() - return parts + yield child + yield loc def relpath(self, start='.'): """Return this path as a relative path, From 6e2d0ba00b60c10466b0e040e2d4b1206c3f0b3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Apr 2021 20:38:58 -0400 Subject: [PATCH 447/835] Remove automerge. Fixes jaraco/skeleton#49. --- .github/workflows/automerge.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/automerge.yml diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml deleted file mode 100644 index 4f70acfb..00000000 --- a/.github/workflows/automerge.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: automerge -on: - pull_request: - types: - - labeled - - unlabeled - - synchronize - - opened - - edited - - ready_for_review - - reopened - - unlocked - pull_request_review: - types: - - submitted - check_suite: - types: - - completed - status: {} -jobs: - automerge: - runs-on: ubuntu-latest - steps: - - name: automerge - uses: "pascalgn/automerge-action@v0.12.0" - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From 2f690f6083feea9a16ea3711f391d598a2ed1228 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Apr 2021 21:19:48 -0400 Subject: [PATCH 448/835] Enable dependabot (#50) * Added a config for dependabot. * Update features list for dependabot. Co-authored-by: KOLANICH --- .github/dependabot.yml | 8 ++++++++ skeleton.md | 1 + 2 files changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/skeleton.md b/skeleton.md index 0938f892..af5f2ca2 100644 --- a/skeleton.md +++ b/skeleton.md @@ -77,6 +77,7 @@ The features/techniques employed by the skeleton include: - A CHANGES.rst file intended for publishing release notes about the project - Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) - Integrated type checking through [mypy](https://github.com/python/mypy/). +- Dependabot enabled to enable supply chain security. ## Packaging Conventions From 6c1c45bc1ce8ab01d91324a46c584172664a0104 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Apr 2021 21:52:29 -0400 Subject: [PATCH 449/835] Replace md file with badge linking to documentation site. Fixes jaraco/skeleton#47. --- README.rst | 3 + skeleton.md | 167 ---------------------------------------------------- 2 files changed, 3 insertions(+), 167 deletions(-) delete mode 100644 skeleton.md diff --git a/README.rst b/README.rst index 128e61e4..a3e1b740 100644 --- a/README.rst +++ b/README.rst @@ -16,3 +16,6 @@ .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton diff --git a/skeleton.md b/skeleton.md deleted file mode 100644 index af5f2ca2..00000000 --- a/skeleton.md +++ /dev/null @@ -1,167 +0,0 @@ -# Overview - -This project is merged with [skeleton](https://github.com/jaraco/skeleton). What is skeleton? It's the scaffolding of a Python project jaraco [introduced in his blog](https://blog.jaraco.com/a-project-skeleton-for-python-projects/). It seeks to provide a means to re-use techniques and inherit advances when managing projects for distribution. - -## An SCM-Managed Approach - -While maintaining dozens of projects in PyPI, jaraco derives best practices for project distribution and publishes them in the [skeleton repo](https://github.com/jaraco/skeleton), a Git repo capturing the evolution and culmination of these best practices. - -It's intended to be used by a new or existing project to adopt these practices and honed and proven techniques. Adopters are encouraged to use the project directly and maintain a small deviation from the technique, make their own fork for more substantial changes unique to their environment or preferences, or simply adopt the skeleton once and abandon it thereafter. - -The primary advantage to using an SCM for maintaining these techniques is that those tools help facilitate the merge between the template and its adopting projects. - -Another advantage to using an SCM-managed approach is that tools like GitHub recognize that a change in the skeleton is the _same change_ across all projects that merge with that skeleton. Without the ancestry, with a traditional copy/paste approach, a [commit like this](https://github.com/jaraco/skeleton/commit/12eed1326e1bc26ce256e7b3f8cd8d3a5beab2d5) would produce notifications in the upstream project issue for each and every application, but because it's centralized, GitHub provides just the one notification when the change is added to the skeleton. - -# Usage - -## new projects - -To use skeleton for a new project, simply pull the skeleton into a new project: - -``` -$ git init my-new-project -$ cd my-new-project -$ git pull gh://jaraco/skeleton -``` - -Now customize the project to suit your individual project needs. - -## existing projects - -If you have an existing project, you can still incorporate the skeleton by merging it into the codebase. - -``` -$ git merge skeleton --allow-unrelated-histories -``` - -The `--allow-unrelated-histories` is necessary because the history from the skeleton was previously unrelated to the existing codebase. Resolve any merge conflicts and commit to the master, and now the project is based on the shared skeleton. - -## Updating - -Whenever a change is needed or desired for the general technique for packaging, it can be made in the skeleton project and then merged into each of the derived projects as needed, recommended before each release. As a result, features and best practices for packaging are centrally maintained and readily trickle into a whole suite of packages. This technique lowers the amount of tedious work necessary to create or maintain a project, and coupled with other techniques like continuous integration and deployment, lowers the cost of creating and maintaining refined Python projects to just a few, familiar Git operations. - -For example, here's a session of the [path project](https://pypi.org/project/path) pulling non-conflicting changes from the skeleton: - - - -Thereafter, the target project can make whatever customizations it deems relevant to the scaffolding. The project may even at some point decide that the divergence is too great to merit renewed merging with the original skeleton. This approach applies maximal guidance while creating minimal constraints. - -## Periodic Collapse - -In late 2020, this project [introduced](https://github.com/jaraco/skeleton/issues/27) the idea of a periodic but infrequent (O(years)) collapse of commits to limit the number of commits a new consumer will need to accept to adopt the skeleton. - -The full history of commits is collapsed into a single commit and that commit becomes the new mainline head. - -When one of these collapse operations happens, any project that previously pulled from the skeleton will no longer have a related history with that new main branch. For those projects, the skeleton provides a "handoff" branch that reconciles the two branches. Any project that has previously merged with the skeleton but now gets an error "fatal: refusing to merge unrelated histories" should instead use the handoff branch once to incorporate the new main branch. - -``` -$ git pull https://github.com/jaraco/skeleton 2020-handoff -``` - -This handoff needs to be pulled just once and thereafter the project can pull from the main head. - -The archive and handoff branches from prior collapses are indicate here: - -| refresh | archive | handoff | -|---------|-----------------|--------------| -| 2020-12 | archive/2020-12 | 2020-handoff | - -# Features - -The features/techniques employed by the skeleton include: - -- PEP 517/518-based build relying on Setuptools as the build tool -- Setuptools declarative configuration using setup.cfg -- tox for running tests -- A README.rst as reStructuredText with some popular badges, but with Read the Docs and AppVeyor badges commented out -- A CHANGES.rst file intended for publishing release notes about the project -- Use of [Black](https://black.readthedocs.io/en/stable/) for code formatting (disabled on unsupported Python 3.5 and earlier) -- Integrated type checking through [mypy](https://github.com/python/mypy/). -- Dependabot enabled to enable supply chain security. - -## Packaging Conventions - -A pyproject.toml is included to enable PEP 517 and PEP 518 compatibility and declares the requirements necessary to build the project on Setuptools (a minimum version compatible with setup.cfg declarative config). - -The setup.cfg file implements the following features: - -- Assumes universal wheel for release -- Advertises the project's LICENSE file (MIT by default) -- Reads the README.rst file into the long description -- Some common Trove classifiers -- Includes all packages discovered in the repo -- Data files in the package are also included (not just Python files) -- Declares the required Python versions -- Declares install requirements (empty by default) -- Declares setup requirements for legacy environments -- Supplies two 'extras': - - testing: requirements for running tests - - docs: requirements for building docs - - these extras split the declaration into "upstream" (requirements as declared by the skeleton) and "local" (those specific to the local project); these markers help avoid merge conflicts -- Placeholder for defining entry points - -Additionally, the setup.py file declares `use_scm_version` which relies on [setuptools_scm](https://pypi.org/project/setuptools_scm) to do two things: - -- derive the project version from SCM tags -- ensure that all files committed to the repo are automatically included in releases - -## Running Tests - -The skeleton assumes the developer has [tox](https://pypi.org/project/tox) installed. The developer is expected to run `tox` to run tests on the current Python version using [pytest](https://pypi.org/project/pytest). - -Other environments (invoked with `tox -e {name}`) supplied include: - - - a `docs` environment to build the documentation - - a `release` environment to publish the package to PyPI - -A pytest.ini is included to define common options around running tests. In particular: - -- rely on default test discovery in the current directory -- avoid recursing into common directories not containing tests -- run doctests on modules and invoke Flake8 tests -- in doctests, allow Unicode literals and regular literals to match, allowing for doctests to run on Python 2 and 3. Also enable ELLIPSES, a default that would be undone by supplying the prior option. -- filters out known warnings caused by libraries/functionality included by the skeleton - -Relies on a .flake8 file to correct some default behaviors: - -- disable mutually incompatible rules W503 and W504 -- support for Black format - -## Continuous Integration - -The project is pre-configured to run Continuous Integration tests. - -### Github Actions - -[Github Actions](https://docs.github.com/en/free-pro-team@latest/actions) are the preferred provider as they provide free, fast, multi-platform services with straightforward configuration. Configured in `.github/workflows`. - -Features include: -- test against multiple Python versions -- run on late (and updated) platform versions -- automated releases of tagged commits -- [automatic merging of PRs](https://github.com/marketplace/actions/merge-pull-requests) (requires [protecting branches with required status checks](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/enabling-required-status-checks), [not possible through API](https://github.community/t/set-all-status-checks-to-be-required-as-branch-protection-using-the-github-api/119493)) - - -### Continuous Deployments - -In addition to running tests, an additional publish stage is configured to automatically release tagged commits to PyPI using [API tokens](https://pypi.org/help/#apitoken). The release process expects an authorized token to be configured with each Github project (or org) `PYPI_TOKEN` [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets). Example: - -``` -pip-run -q jaraco.develop -- -m jaraco.develop.add-github-secrets -``` - -## Building Documentation - -Documentation is automatically built by [Read the Docs](https://readthedocs.org) when the project is registered with it, by way of the .readthedocs.yml file. To test the docs build manually, a tox env may be invoked as `tox -e docs`. Both techniques rely on the dependencies declared in `setup.cfg/options.extras_require.docs`. - -In addition to building the Sphinx docs scaffolded in `docs/`, the docs build a `history.html` file that first injects release dates and hyperlinks into the CHANGES.rst before incorporating it as history in the docs. - -## Cutting releases - -By default, tagged commits are released through the continuous integration deploy stage. - -Releases may also be cut manually by invoking the tox environment `release` with the PyPI token set as the TWINE_PASSWORD: - -``` -TWINE_PASSWORD={token} tox -e release -``` From 8698127dbd17b47d1d07e35bee3725fecb69670b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 26 Apr 2021 04:10:20 +0200 Subject: [PATCH 450/835] Make sphinx fail on any warnings (#36) This change adds `nitpicky=True` (which is an equivalent of `-n`) to make Sphinx emit warnings for any references to non-existing targets. Then, it adds `-W` to make it fail whenever a single warning is seen. Finally, `--keep-going` allows Sphinx to print out all the warnings before exiting instead of showing just one and bailing. Resolves #29 Refs: * https://www.sphinx-doc.org/en/master/man/sphinx-build.html#cmdoption-sphinx-build-n * https://www.sphinx-doc.org/en/master/man/sphinx-build.html#cmdoption-sphinx-build-W * https://www.sphinx-doc.org/en/master/man/sphinx-build.html#cmdoption-sphinx-build-keep-going --- docs/conf.py | 3 +++ tox.ini | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 433d185d..f65d1faa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,3 +24,6 @@ ], ) } + +# Be strict about any broken references: +nitpicky = True diff --git a/tox.ini b/tox.ini index a9a50b01..69848905 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ extras = testing changedir = docs commands = - python -m sphinx . {toxinidir}/build/html + python -m sphinx -W --keep-going . {toxinidir}/build/html [testenv:release] skip_install = True From 4a734d4841b0ad5fddad3c2524e512f608c82d74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 May 2021 14:01:53 -0400 Subject: [PATCH 451/835] Test on Python 3.10 --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a8ff006..7d6b455b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,10 @@ jobs: test: strategy: matrix: - python: [3.6, 3.8, 3.9] + python: + - 3.6 + - 3.9 + - 3.10.0-alpha - 3.10.99 platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From 1b165200642e74a4c2acebf7fedb28e732a17881 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 26 May 2021 10:40:46 -0400 Subject: [PATCH 452/835] Remove setup_requires, obviated by build-requires in pyproject.toml. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 52876d55..e768c6b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,6 @@ packages = find_namespace: include_package_data = true python_requires = >=3.6 install_requires = -setup_requires = setuptools_scm[toml] >= 3.4.1 [options.packages.find] exclude = From 85d08db3ef3811bd208995254e7e9c9658cf710d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 3 Jun 2021 19:55:51 -0400 Subject: [PATCH 453/835] Suppress deprecation warnings in flake8 and packaging.tags. Ref pypa/packaging#433. --- pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytest.ini b/pytest.ini index 6bf69af1..31b114fd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,3 +5,7 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS # workaround for warning pytest-dev/pytest#6178 junit_family=xunit2 filterwarnings= + # Suppress deprecation warning in flake8 + ignore:SelectableGroups dict interface is deprecated::flake8 + # Suppress deprecation warning in pypa/packaging#433 + ignore:The distutils package is deprecated::packaging.tags From 5a8384e53c59a886f982739c02572732afa76c7f Mon Sep 17 00:00:00 2001 From: Brian Rutledge Date: Sat, 12 Jun 2021 10:10:07 -0400 Subject: [PATCH 454/835] Use shutil for rmtree --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 69848905..3ca2af38 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,6 @@ skip_install = True deps = build twine>=3 - path jaraco.develop>=7.1 passenv = TWINE_PASSWORD @@ -34,7 +33,7 @@ passenv = setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = - python -c "import path; path.Path('dist').rmtree_p()" + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release From 8f5adebcaf9b43c765835a9d391bb5ca65d3a67f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 19:25:10 -0400 Subject: [PATCH 455/835] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.py | 68 ++++++++++++++++++++++++------------------------ test_path.py | 8 +++--- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8336d2b1..6486409a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -243,36 +243,36 @@ def getcwd(cls): # --- Operations on Path strings. def abspath(self): - """ .. seealso:: :func:`os.path.abspath` """ + """.. seealso:: :func:`os.path.abspath`""" return self._next_class(self.module.abspath(self)) def normcase(self): - """ .. seealso:: :func:`os.path.normcase` """ + """.. seealso:: :func:`os.path.normcase`""" return self._next_class(self.module.normcase(self)) def normpath(self): - """ .. seealso:: :func:`os.path.normpath` """ + """.. seealso:: :func:`os.path.normpath`""" return self._next_class(self.module.normpath(self)) def realpath(self): - """ .. seealso:: :func:`os.path.realpath` """ + """.. seealso:: :func:`os.path.realpath`""" realpath = best_realpath(self.module) return self._next_class(realpath(self)) def expanduser(self): - """ .. seealso:: :func:`os.path.expanduser` """ + """.. seealso:: :func:`os.path.expanduser`""" return self._next_class(self.module.expanduser(self)) def expandvars(self): - """ .. seealso:: :func:`os.path.expandvars` """ + """.. seealso:: :func:`os.path.expandvars`""" return self._next_class(self.module.expandvars(self)) def dirname(self): - """ .. seealso:: :attr:`parent`, :func:`os.path.dirname` """ + """.. seealso:: :attr:`parent`, :func:`os.path.dirname`""" return self._next_class(self.module.dirname(self)) def basename(self): - """ .. seealso:: :attr:`name`, :func:`os.path.basename` """ + """.. seealso:: :attr:`name`, :func:`os.path.basename`""" return self._next_class(self.module.basename(self)) def expand(self): @@ -296,7 +296,7 @@ def stem(self): @property def ext(self): - """ The file extension, for example ``'.py'``. """ + """The file extension, for example ``'.py'``.""" f, ext = self.module.splitext(self) return ext @@ -399,7 +399,7 @@ def stripext(self): return self.splitext()[0] def splitunc(self): - """ .. seealso:: :func:`os.path.splitunc` """ + """.. seealso:: :func:`os.path.splitunc`""" unc, rest = self.module.splitunc(self) return self._next_class(unc), rest @@ -656,7 +656,7 @@ def open(self, *args, **kwargs): return io.open(self, *args, **kwargs) def bytes(self): - """ Open this file, read all bytes, return them as a string. """ + """Open this file, read all bytes, return them as a string.""" with self.open('rb') as f: return f.read() @@ -904,38 +904,38 @@ def read_hexhash(self, hash_name): # bound. Playing it safe and wrapping them all in method calls. def isabs(self): - """ .. seealso:: :func:`os.path.isabs` """ + """.. seealso:: :func:`os.path.isabs`""" return self.module.isabs(self) def exists(self): - """ .. seealso:: :func:`os.path.exists` """ + """.. seealso:: :func:`os.path.exists`""" return self.module.exists(self) def isdir(self): - """ .. seealso:: :func:`os.path.isdir` """ + """.. seealso:: :func:`os.path.isdir`""" return self.module.isdir(self) def isfile(self): - """ .. seealso:: :func:`os.path.isfile` """ + """.. seealso:: :func:`os.path.isfile`""" return self.module.isfile(self) def islink(self): - """ .. seealso:: :func:`os.path.islink` """ + """.. seealso:: :func:`os.path.islink`""" return self.module.islink(self) def ismount(self): - """ .. seealso:: :func:`os.path.ismount` """ + """.. seealso:: :func:`os.path.ismount`""" return self.module.ismount(self) def samefile(self, other): - """ .. seealso:: :func:`os.path.samefile` """ + """.. seealso:: :func:`os.path.samefile`""" if not hasattr(self.module, 'samefile'): other = Path(other).realpath().normpath().normcase() return self.realpath().normpath().normcase() == other return self.module.samefile(self, other) def getatime(self): - """ .. seealso:: :attr:`atime`, :func:`os.path.getatime` """ + """.. seealso:: :attr:`atime`, :func:`os.path.getatime`""" return self.module.getatime(self) atime = property( @@ -949,7 +949,7 @@ def getatime(self): ) def getmtime(self): - """ .. seealso:: :attr:`mtime`, :func:`os.path.getmtime` """ + """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`""" return self.module.getmtime(self) mtime = property( @@ -963,7 +963,7 @@ def getmtime(self): ) def getctime(self): - """ .. seealso:: :attr:`ctime`, :func:`os.path.getctime` """ + """.. seealso:: :attr:`ctime`, :func:`os.path.getctime`""" return self.module.getctime(self) ctime = property( @@ -977,7 +977,7 @@ def getctime(self): ) def getsize(self): - """ .. seealso:: :attr:`size`, :func:`os.path.getsize` """ + """.. seealso:: :attr:`size`, :func:`os.path.getsize`""" return self.module.getsize(self) size = property( @@ -1073,7 +1073,7 @@ def statvfs(self): if hasattr(os, 'pathconf'): def pathconf(self, name): - """ .. seealso:: :func:`os.pathconf` """ + """.. seealso:: :func:`os.pathconf`""" return os.pathconf(self, name) # @@ -1118,12 +1118,12 @@ def chown(self, uid=-1, gid=-1): return self def rename(self, new): - """ .. seealso:: :func:`os.rename` """ + """.. seealso:: :func:`os.rename`""" os.rename(self, new) return self._next_class(new) def renames(self, new): - """ .. seealso:: :func:`os.renames` """ + """.. seealso:: :func:`os.renames`""" os.renames(self, new) return self._next_class(new) @@ -1131,7 +1131,7 @@ def renames(self, new): # --- Create/delete operations on directories def mkdir(self, mode=0o777): - """ .. seealso:: :func:`os.mkdir` """ + """.. seealso:: :func:`os.mkdir`""" os.mkdir(self, mode) return self @@ -1143,7 +1143,7 @@ def mkdir_p(self, mode=0o777): return self def makedirs(self, mode=0o777): - """ .. seealso:: :func:`os.makedirs` """ + """.. seealso:: :func:`os.makedirs`""" os.makedirs(self, mode) return self @@ -1155,7 +1155,7 @@ def makedirs_p(self, mode=0o777): return self def rmdir(self): - """ .. seealso:: :func:`os.rmdir` """ + """.. seealso:: :func:`os.rmdir`""" os.rmdir(self) return self @@ -1169,7 +1169,7 @@ def rmdir_p(self): return self def removedirs(self): - """ .. seealso:: :func:`os.removedirs` """ + """.. seealso:: :func:`os.removedirs`""" os.removedirs(self) return self @@ -1193,7 +1193,7 @@ def touch(self): return self def remove(self): - """ .. seealso:: :func:`os.remove` """ + """.. seealso:: :func:`os.remove`""" os.remove(self) return self @@ -1205,7 +1205,7 @@ def remove_p(self): return self def unlink(self): - """ .. seealso:: :func:`os.unlink` """ + """.. seealso:: :func:`os.unlink`""" os.unlink(self) return self @@ -1288,7 +1288,7 @@ def rmtree_p(self): return self def chdir(self): - """ .. seealso:: :func:`os.chdir` """ + """.. seealso:: :func:`os.chdir`""" os.chdir(self) cd = chdir @@ -1346,13 +1346,13 @@ def ignored(item): if hasattr(os, 'chroot'): def chroot(self): - """ .. seealso:: :func:`os.chroot` """ + """.. seealso:: :func:`os.chroot`""" os.chroot(self) if hasattr(os, 'startfile'): def startfile(self): - """ .. seealso:: :func:`os.startfile` """ + """.. seealso:: :func:`os.startfile`""" os.startfile(self) return self diff --git a/test_path.py b/test_path.py index a6f1737d..6862be03 100644 --- a/test_path.py +++ b/test_path.py @@ -40,7 +40,7 @@ def os_choose(**choices): - """ Choose a value from several possible values, based on os.name """ + """Choose a value from several possible values, based on os.name""" return choices[os.name] @@ -79,7 +79,7 @@ def test_relpath(self): assert d.relpathto(boz) == boz def test_construction_from_none(self): - """""" + """ """ with pytest.raises(TypeError): Path(None) @@ -90,7 +90,7 @@ def test_construction_from_int(self): assert Path(1) == '1' def test_string_compatibility(self): - """ Test compatibility with ordinary strings. """ + """Test compatibility with ordinary strings.""" x = Path('xyzzy') assert x == 'xyzzy' assert x == str('xyzzy') @@ -836,7 +836,7 @@ def test_only_newer(self): class TestChdir: def test_chdir_or_cd(self, tmpdir): - """ tests the chdir or cd method """ + """tests the chdir or cd method""" d = Path(str(tmpdir)) cwd = d.getcwd() From 4005836362977b889f5941175a121f2c8cb2abb7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 19:35:02 -0400 Subject: [PATCH 456/835] Add test capturing missed expectation. Ref #200. --- test_path.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test_path.py b/test_path.py index 6862be03..5fa800aa 100644 --- a/test_path.py +++ b/test_path.py @@ -931,6 +931,13 @@ def test_context_manager_using_with(self): assert d.isdir() assert not d.isdir() + def test_cleaned_up_on_interrupt(self): + with contextlib.suppress(KeyboardInterrupt): + with TempDir() as d: + raise KeyboardInterrupt() + + assert not d.exists() + class TestUnicode: @pytest.fixture(autouse=True) From 4a33b540caff5cbc78d07e44f7f94f1498efbc63 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 19:37:53 -0400 Subject: [PATCH 457/835] Clean up unconditionally. Fixes #200. --- path/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 6486409a..a2f706e6 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1632,8 +1632,7 @@ def __enter__(self): return self._next_class(self) def __exit__(self, exc_type, exc_value, traceback): - if not exc_value: - self.rmtree() + self.rmtree() def _multi_permission_mask(mode): From 117f01833bbdc5c21fdf78796e8d5c737f803a9f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 20:07:24 -0400 Subject: [PATCH 458/835] Remove prior expectation that an exception causes a TempDir not to be cleaned up. Ref #200. --- test_path.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test_path.py b/test_path.py index 5fa800aa..e5f656b1 100644 --- a/test_path.py +++ b/test_path.py @@ -910,17 +910,6 @@ def test_context_manager(self): d.__exit__(None, None, None) assert not d.exists() - def test_context_manager_exception(self): - """ - The context manager will not clean up if an exception occurs. - """ - d = TempDir() - d.__enter__() - (d / 'somefile.txt').touch() - assert not isinstance(d / 'somefile.txt', TempDir) - d.__exit__(TypeError, TypeError('foo'), None) - assert d.exists() - def test_context_manager_using_with(self): """ The context manager will allow using the with keyword and From ea0c6e46ed1da368b05695062992334734734015 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 20:18:37 -0400 Subject: [PATCH 459/835] Update changelog. Ref #200. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 51011721..a2bc6c1e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v16.0.0 +------- + +- #200: ``TempDir`` context now cleans up unconditionally, + even if an exception occurs. + v15.1.2 ------- From f3d61fb61a675fc4f822d7993e0f3e41e44a96ad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 20:23:56 -0400 Subject: [PATCH 460/835] Fix Sphinx warnings reported in #201. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index a2f706e6..9ba1b40c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -707,7 +707,7 @@ def read_bytes(self): return f.read() def text(self, encoding=None, errors='strict'): - """Legacy function to read text. + r"""Legacy function to read text. Converts all newline sequences to ``\n``. """ From d575110f26c4bffa98b1a54b5d66c454bcc696ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 20:31:34 -0400 Subject: [PATCH 461/835] Rewrite docstrings now that Sphinx interprets '->' as Python syntax. Ref #201. --- path/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 9ba1b40c..16452886 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -356,7 +356,7 @@ def drive(self): ) def splitpath(self): - """p.splitpath() -> Return ``(p.parent, p.name)``. + """Return two-tuple of ``.parent``, ``.name``. .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split` """ @@ -364,7 +364,7 @@ def splitpath(self): return self._next_class(parent), child def splitdrive(self): - """p.splitdrive() -> Return ``(p.drive, )``. + """Return two-tuple of ``.drive`` and rest without drive. Split the drive specifier from this path. If there is no drive specifier, :samp:`{p.drive}` is empty, so the return value @@ -376,7 +376,7 @@ def splitdrive(self): return self._next_class(drive), rel def splitext(self): - """p.splitext() -> Return ``(p.stripext(), p.ext)``. + """Return two-tuple of ``.stripext()`` and ``.ext``. Split the filename extension from this path and return the two parts. Either part may be empty. @@ -391,7 +391,7 @@ def splitext(self): return self._next_class(filename), ext def stripext(self): - """p.stripext() -> Remove one file extension from the path. + """Remove one file extension from the path. For example, ``Path('/home/guido/python.tar.gz').stripext()`` returns ``Path('/home/guido/python.tar')``. @@ -505,7 +505,7 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching def listdir(self, match=None): - """D.listdir() -> List of items in this directory. + """List of items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing of just files or just subdirectories. @@ -521,7 +521,7 @@ def listdir(self, match=None): return list(filter(match, (self / child for child in os.listdir(self)))) def dirs(self, *args, **kwargs): - """D.dirs() -> List of this directory's subdirectories. + """List of this directory's subdirectories. The elements of the list are Path objects. This does not walk recursively into subdirectories @@ -532,7 +532,7 @@ def dirs(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isdir()] def files(self, *args, **kwargs): - """D.files() -> List of the files in this directory. + """List of the files in self. The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). @@ -543,7 +543,7 @@ def files(self, *args, **kwargs): return [p for p in self.listdir(*args, **kwargs) if p.isfile()] def walk(self, match=None, errors='strict'): - """D.walk() -> iterator over files and subdirs, recursively. + """Iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of this directory and its descendants. This requires that @@ -583,11 +583,11 @@ def walk(self, match=None, errors='strict'): yield item def walkdirs(self, *args, **kwargs): - """D.walkdirs() -> iterator over subdirs, recursively.""" + """Iterator over subdirs, recursively.""" return (item for item in self.walk(*args, **kwargs) if item.isdir()) def walkfiles(self, *args, **kwargs): - """D.walkfiles() -> iterator over files in D, recursively.""" + """Iterator over files, recursively.""" return (item for item in self.walk(*args, **kwargs) if item.isfile()) def fnmatch(self, pattern, normcase=None): From 8aea87c12eae20533a58fe730abb113c0b7d5160 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Jun 2021 20:52:58 -0400 Subject: [PATCH 462/835] Fix other sphinx warnings. Fixes #201. --- path/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 16452886..4ded5847 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -235,7 +235,7 @@ def __fspath__(self): def getcwd(cls): """Return the current working directory as a path object. - .. seealso:: :func:`os.getcwdu` + .. seealso:: :func:`os.getcwd` """ return cls(os.getcwd()) @@ -399,7 +399,6 @@ def stripext(self): return self.splitext()[0] def splitunc(self): - """.. seealso:: :func:`os.path.splitunc`""" unc, rest = self.module.splitunc(self) return self._next_class(unc), rest @@ -598,8 +597,8 @@ def fnmatch(self, pattern, normcase=None): attribute, it is applied to the name and path prior to comparison. `normcase` - (optional) A function used to normalize the pattern and - filename before matching. Defaults to :meth:`self.module`, which - defaults to :meth:`os.path.normcase`. + filename before matching. Defaults to normcase from + ``self.module``, :func:`os.path.normcase`. .. seealso:: :func:`fnmatch.fnmatch` """ @@ -648,10 +647,10 @@ def iglob(self, pattern): # --- Reading or writing an entire file at once. def open(self, *args, **kwargs): - """Open this file and return a corresponding :class:`file` object. + """Open this file and return a corresponding file object. Keyword arguments work as in :func:`io.open`. If the file cannot be - opened, an :class:`~exceptions.OSError` is raised. + opened, an :class:`OSError` is raised. """ return io.open(self, *args, **kwargs) @@ -835,7 +834,7 @@ def write_lines( default is os.linesep, which is platform-dependent (``'\r\n'`` on Windows, ``'\n'`` on Unix, etc.). Specify ``None`` to write the lines as-is, like - :meth:`file.writelines`. + ``.writelines`` on a file object. Use the keyword argument ``append=True`` to append lines to the file. The default is to overwrite the file. From 14787e69e793d68c8ac17f010dc45891ee0a492c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jul 2021 09:03:21 -0400 Subject: [PATCH 463/835] Rely on setuptools 56 and drop the explicit mention of the license file in favor of simple discovery. --- pyproject.toml | 2 +- setup.cfg | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b6ebc0be..28bd7883 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=56", "wheel", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] diff --git a/setup.cfg b/setup.cfg index e768c6b3..53387b60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,4 @@ [metadata] -license_files = - LICENSE name = skeleton author = Jason R. Coombs author_email = jaraco@jaraco.com From 212e995cd366010a8c372ea2fedfbb8be471e5cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jul 2021 09:07:59 -0400 Subject: [PATCH 464/835] Remove workaround for python/typed_ast#156. --- setup.cfg | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 53387b60..80fd268f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,12 +32,10 @@ testing = pytest-checkdocs >= 2.4 pytest-flake8 # python_implementation: workaround for jaraco/skeleton#22 - # python_version: workaround for python/typed_ast#156 - pytest-black >= 0.3.7; python_implementation != "PyPy" and python_version < "3.10" + pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov # python_implementation: workaround for jaraco/skeleton#22 - # python_version: workaround for python/typed_ast#156 - pytest-mypy; python_implementation != "PyPy" and python_version < "3.10" + pytest-mypy; python_implementation != "PyPy" pytest-enabler >= 1.0.1 # local From 498b965a805224420c8cde5969bf342a41766227 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jul 2021 09:19:05 -0400 Subject: [PATCH 465/835] Use line continuations to indicate which exclusions are for which workarounds. --- setup.cfg | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 80fd268f..69eb0ee6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,11 +31,13 @@ testing = pytest >= 4.6 pytest-checkdocs >= 2.4 pytest-flake8 - # python_implementation: workaround for jaraco/skeleton#22 - pytest-black >= 0.3.7; python_implementation != "PyPy" + pytest-black >= 0.3.7; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" pytest-cov - # python_implementation: workaround for jaraco/skeleton#22 - pytest-mypy; python_implementation != "PyPy" + pytest-mypy; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" pytest-enabler >= 1.0.1 # local From 918a415b41225f442d6e9b319ecebff19b52a1a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jul 2021 14:48:16 -0400 Subject: [PATCH 466/835] Implement tidelift banner using a Sphinx directive implemented by jaraco.tidelift. --- docs/_templates/tidelift-sidebar.html | 6 ------ docs/conf.py | 6 +----- docs/index.rst | 1 + setup.cfg | 4 ++++ 4 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 docs/_templates/tidelift-sidebar.html create mode 100644 docs/index.rst create mode 100644 setup.cfg diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html deleted file mode 100644 index ce48f46b..00000000 --- a/docs/_templates/tidelift-sidebar.html +++ /dev/null @@ -1,6 +0,0 @@ -

For Enterprise

- -

-Professionally-supported {{ project }} is available with the -Tidelift Subscription. -

diff --git a/docs/conf.py b/docs/conf.py index dbf962dd..bd8564ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1 @@ - -# Custom sidebar templates, maps document names to template names. -html_theme = 'alabaster' -templates_path = ['_templates'] -html_sidebars = {'index': ['tidelift-sidebar.html']} +extensions += ['jaraco.tidelift'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..6e321fdc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1 @@ +.. tidelift-referral-banner:: diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..798b1033 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[options.extras_require] +docs = + # upstream + jaraco.tidelift >= 1.4 From 719a7ced8a1713b7fe94d842a8f6fec7425b8a0a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Jul 2021 17:33:29 -0400 Subject: [PATCH 467/835] Remove blacken docs as it cannot honor Python's default repr. Ref asottile/blacken-docs#62. --- .pre-commit-config.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c15ab0c9..f66bf563 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,3 @@ repos: rev: 20.8b1 hooks: - id: black - -- repo: https://github.com/asottile/blacken-docs - rev: v1.9.1 - hooks: - - id: blacken-docs From fbf78968730d38512b7a4713905f3af0f6fd34be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 19:58:28 -0400 Subject: [PATCH 468/835] Re-use compose from jaraco.functools. --- path/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 4ded5847..a015bb35 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1638,14 +1638,10 @@ def _multi_permission_mask(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. - >>> _multi_permission_mask('a=r,u+w')(0) == 0o644 - True + >>> oct(_multi_permission_mask('a=r,u+w')(0)) + '0o644' """ - - def compose(f, g): - return lambda *args, **kwargs: g(f(*args, **kwargs)) - - return functools.reduce(compose, map(_permission_mask, mode.split(','))) + return compose(*map(_permission_mask, reversed(mode.split(',')))) def _permission_mask(mode): From 6ee90c43dd56e24f6beeef51d8c5aefd9b699d5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:01:57 -0400 Subject: [PATCH 469/835] Remove exception handler for 'Python bug', apparently no longer relevant as it goes back at least 12 years. --- path/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index a015bb35..998f5ca3 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -182,10 +182,7 @@ def __repr__(self): # Adding a Path and a string yields a Path. def __add__(self, more): - try: - return self._next_class(super(Path, self).__add__(more)) - except TypeError: # Python bug - return NotImplemented + return self._next_class(super(Path, self).__add__(more)) def __radd__(self, other): if not isinstance(other, str): From 2004d3b2038293929ca138490747bfce36be14ad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:08:17 -0400 Subject: [PATCH 470/835] Remove another unused block. --- path/__init__.py | 2 -- test_path.py | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 998f5ca3..1599a0dd 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -185,8 +185,6 @@ def __add__(self, more): return self._next_class(super(Path, self).__add__(more)) def __radd__(self, other): - if not isinstance(other, str): - return NotImplemented return self._next_class(other.__add__(self)) # The / operator joins Paths. diff --git a/test_path.py b/test_path.py index e5f656b1..baa28a47 100644 --- a/test_path.py +++ b/test_path.py @@ -196,6 +196,10 @@ def test_joinpath_returns_same_type(self): assert isinstance(res2, path_posix) assert res2 == 'foo/bar' + def test_radd_string(self): + res = 'foo' + Path('bar') + assert res == Path('foobar') + class TestReadWriteText: def test_read_write(self, tmpdir): From f03e178afd94f6adfdcc882783c08afe2e737d84 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:17:02 -0400 Subject: [PATCH 471/835] Remove __fspath__ as it is unneeded as Path is a str subclass. Add a test to capture expected behavior. --- path/__init__.py | 3 --- test_path.py | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1599a0dd..13785ecb 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -223,9 +223,6 @@ def __enter__(self): def __exit__(self, *_): os.chdir(self._old_dir) - def __fspath__(self): - return self - @classmethod def getcwd(cls): """Return the current working directory as a path object. diff --git a/test_path.py b/test_path.py index baa28a47..888ba33a 100644 --- a/test_path.py +++ b/test_path.py @@ -200,6 +200,9 @@ def test_radd_string(self): res = 'foo' + Path('bar') assert res == Path('foobar') + def test_fspath(self): + os.fspath(Path('foobar')) + class TestReadWriteText: def test_read_write(self, tmpdir): From c199b5cc605718efeba943707a4d773b0c517df9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:32:19 -0400 Subject: [PATCH 472/835] Add test for normpath --- test_path.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_path.py b/test_path.py index 888ba33a..ea8697f5 100644 --- a/test_path.py +++ b/test_path.py @@ -203,6 +203,9 @@ def test_radd_string(self): def test_fspath(self): os.fspath(Path('foobar')) + def test_normpath(self): + assert Path('foo//bar').normpath() == os.path.normpath('foo//bar') + class TestReadWriteText: def test_read_write(self, tmpdir): From c2d1817564a22009abe56894524d4b8fdc8fdfea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:36:23 -0400 Subject: [PATCH 473/835] Add test for expandvars --- test_path.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test_path.py b/test_path.py index ea8697f5..ed9aa9e4 100644 --- a/test_path.py +++ b/test_path.py @@ -206,6 +206,12 @@ def test_fspath(self): def test_normpath(self): assert Path('foo//bar').normpath() == os.path.normpath('foo//bar') + def test_expandvars(self, monkeypatch): + monkeypatch.setitem(os.environ, 'sub', 'value') + val = '$sub/$(sub)' + assert Path(val).expandvars() == os.path.expandvars(val) + assert 'value' in Path(val).expandvars() + class TestReadWriteText: def test_read_write(self, tmpdir): From 557f5153da08b9aa9033fa1d5dcfb38f064f4a5c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:42:08 -0400 Subject: [PATCH 474/835] Add test for .expand --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index ed9aa9e4..cb70a74c 100644 --- a/test_path.py +++ b/test_path.py @@ -212,6 +212,11 @@ def test_expandvars(self, monkeypatch): assert Path(val).expandvars() == os.path.expandvars(val) assert 'value' in Path(val).expandvars() + def test_expand(self): + val = 'foobar' + expected = os.path.normpath(os.path.expanduser(os.path.expandvars(val))) + assert Path(val).expand() == expected + class TestReadWriteText: def test_read_write(self, tmpdir): From e7df22f1e8654d0eafe683035b7408651d27728e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 20:49:26 -0400 Subject: [PATCH 475/835] Add test for splitdrive --- test_path.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test_path.py b/test_path.py index cb70a74c..1cd711fc 100644 --- a/test_path.py +++ b/test_path.py @@ -217,6 +217,12 @@ def test_expand(self): expected = os.path.normpath(os.path.expanduser(os.path.expandvars(val))) assert Path(val).expand() == expected + def test_splitdrive(self): + val = Path.using_module(ntpath)(r'C:\bar') + drive, rest = val.splitdrive() + assert drive == 'C:' + assert rest == Path(r'\bar') + class TestReadWriteText: def test_read_write(self, tmpdir): From ddc66a8f80618dfb91dec293973228d20429c344 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 21:00:30 -0400 Subject: [PATCH 476/835] Expand test to ensure the result is a Path. Fix test and make both parts Path objects. --- path/__init__.py | 2 +- test_path.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 13785ecb..1b2de7d4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -365,7 +365,7 @@ def splitdrive(self): .. seealso:: :func:`os.path.splitdrive` """ drive, rel = self.module.splitdrive(self) - return self._next_class(drive), rel + return self._next_class(drive), self._next_class(rel) def splitext(self): """Return two-tuple of ``.stripext()`` and ``.ext``. diff --git a/test_path.py b/test_path.py index 1cd711fc..f602f61a 100644 --- a/test_path.py +++ b/test_path.py @@ -221,7 +221,8 @@ def test_splitdrive(self): val = Path.using_module(ntpath)(r'C:\bar') drive, rest = val.splitdrive() assert drive == 'C:' - assert rest == Path(r'\bar') + assert rest == r'\bar' + assert isinstance(rest, Path) class TestReadWriteText: From 0dbd78bb41faaa2c8bb94df9fa9fa01b3ad36642 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 21:12:04 -0400 Subject: [PATCH 477/835] Remove unc functions, removed in Python 3.7 (bpo-29197). --- path/__init__.py | 13 ------------- test_path.py | 6 ------ 2 files changed, 19 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1b2de7d4..1ada9cf7 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -390,19 +390,6 @@ def stripext(self): """ return self.splitext()[0] - def splitunc(self): - unc, rest = self.module.splitunc(self) - return self._next_class(unc), rest - - @property - def uncshare(self): - """ - The UNC mount point for this path. - This is empty for paths on local drives. - """ - unc, r = self.module.splitunc(self) - return self._next_class(unc) - @multimethod def joinpath(cls, first, *others): """ diff --git a/test_path.py b/test_path.py index f602f61a..8a1eb6bf 100644 --- a/test_path.py +++ b/test_path.py @@ -140,12 +140,6 @@ def test_methods(self): assert isinstance(cwd, Path) assert cwd == os.getcwd() - @pytest.mark.skipif('not hasattr(os.path, "splitunc")') - def test_UNC(self): - p = Path(r'\\python1\share1\dir1\file1.txt') - assert p.uncshare == r'\\python1\share1' - assert p.splitunc() == os.path.splitunc(str(p)) - def test_explicit_module(self): """ The user may specify an explicit path module to use. From 125e743b45be054bee11c201de42c46036c436d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 21:14:22 -0400 Subject: [PATCH 478/835] Add test for parts --- path/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index 1ada9cf7..15287b4f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -420,6 +420,10 @@ def splitall(self): return list(self._parts()) def parts(self): + """ + >>> Path('/foo/bar/baz').parts() + (Path('/'), 'foo', 'bar', 'baz') + """ return tuple(self._parts()) def _parts(self): From 7922165de86055fa8f6291dcafc9344188085349 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 21:18:36 -0400 Subject: [PATCH 479/835] Add test covering relpathto --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index 8a1eb6bf..7bc6b00d 100644 --- a/test_path.py +++ b/test_path.py @@ -218,6 +218,11 @@ def test_splitdrive(self): assert rest == r'\bar' assert isinstance(rest, Path) + def test_relpathto(self): + source = Path.using_module(ntpath)(r'C:\foo') + dest = Path.using_module(ntpath)(r'D:\bar') + assert source.relpathto(dest) == dest + class TestReadWriteText: def test_read_write(self, tmpdir): From a1356defc93708c6de9969d19447cc7ad5bc0a0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 22:01:04 -0400 Subject: [PATCH 480/835] Add tests covering walk. Fix bug in walk when errors are ignored. --- path/__init__.py | 1 + test_path.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index 15287b4f..e6dd6f42 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -559,6 +559,7 @@ def walk(self, match=None, errors='strict'): do_traverse = traverse() except Exception as exc: errors(f"Unable to access '{child}': {exc}") + continue if do_traverse: for item in child.walk(errors=errors, match=match): diff --git a/test_path.py b/test_path.py index 7bc6b00d..4d886cf8 100644 --- a/test_path.py +++ b/test_path.py @@ -223,6 +223,22 @@ def test_relpathto(self): dest = Path.using_module(ntpath)(r'D:\bar') assert source.relpathto(dest) == dest + def test_walk_errors(self): + start = Path('/does-not-exist') + items = list(start.walk(errors='ignore')) + assert not items + + def test_walk_child_error(self, tmpdir): + def simulate_access_denied(item): + if item.name == 'sub1': + raise OSError("Access denied") + return True + + p = Path(tmpdir) + (p / 'sub1').makedirs_p() + items = path.Traversal(simulate_access_denied)(p.walk(errors='ignore')) + assert list(items) == [p / 'sub1'] + class TestReadWriteText: def test_read_write(self, tmpdir): From e3fc901f36abd9927f6650a194e31ac06e10932f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 22:16:01 -0400 Subject: [PATCH 481/835] Add test covering write test. Fix resulting failures. --- path/__init__.py | 10 +++++----- test_path.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index e6dd6f42..f9dd6d89 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -57,10 +57,10 @@ LINESEPS = ['\r\n', '\r', '\n'] U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] -NEWLINE = re.compile('|'.join(LINESEPS)) U_NEWLINE = re.compile('|'.join(U_LINESEPS)) -NL_END = re.compile(r'(?:{0})$'.format(NEWLINE.pattern)) -U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern)) +B_NEWLINE = re.compile('|'.join(LINESEPS).encode()) +B_NL_END = re.compile(f'(?:{B_NEWLINE.pattern.decode()})$'.encode()) +U_NL_END = re.compile(f'(?:{U_NEWLINE.pattern})$') class TreeWalkWarning(Warning): @@ -769,7 +769,7 @@ def write_text( text = text.encode(encoding or sys.getdefaultencoding(), errors) else: assert encoding is None - text = NEWLINE.sub(linesep, text) + text = B_NEWLINE.sub(linesep.encode(), text) self.write_bytes(text, append=append) def lines(self, encoding=None, errors='strict', retain=True): @@ -835,7 +835,7 @@ def write_lines( for line in lines: isUnicode = isinstance(line, str) if linesep is not None: - pattern = U_NL_END if isUnicode else NL_END + pattern = U_NL_END if isUnicode else B_NL_END line = pattern.sub('', line) + linesep if isUnicode: line = line.encode(encoding or sys.getdefaultencoding(), errors) diff --git a/test_path.py b/test_path.py index 4d886cf8..a56443c5 100644 --- a/test_path.py +++ b/test_path.py @@ -246,6 +246,7 @@ def test_read_write(self, tmpdir): file.write_text('hello world') assert file.read_text() == 'hello world' assert file.read_bytes() == b'hello world' + file.write_text(b'hello world') class TestPerformance: From f289a7bdbf4d1ebc946b05c7b6a78a268294197b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 22:19:42 -0400 Subject: [PATCH 482/835] Add test covering read_md5 --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index a56443c5..918181d6 100644 --- a/test_path.py +++ b/test_path.py @@ -239,6 +239,11 @@ def simulate_access_denied(item): items = path.Traversal(simulate_access_denied)(p.walk(errors='ignore')) assert list(items) == [p / 'sub1'] + def test_read_md5(self, tmpdir): + target = Path(tmpdir) / 'some file' + target.write_text('quick brown fox and lazy dog') + assert target.read_md5() == b's\x15\rPOW\x7fYk\xa8\x8e\x00\x0b\xd7G\xf9' + class TestReadWriteText: def test_read_write(self, tmpdir): From 98aa2120a9d7c68a52ff5bbd8088061562678312 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 24 Jul 2021 22:21:32 -0400 Subject: [PATCH 483/835] Add test covering read_hexhash --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index 918181d6..230c4a93 100644 --- a/test_path.py +++ b/test_path.py @@ -244,6 +244,11 @@ def test_read_md5(self, tmpdir): target.write_text('quick brown fox and lazy dog') assert target.read_md5() == b's\x15\rPOW\x7fYk\xa8\x8e\x00\x0b\xd7G\xf9' + def test_read_hexhash(self, tmpdir): + target = Path(tmpdir) / 'some file' + target.write_text('quick brown fox and lazy dog') + assert target.read_hexhash('md5') == '73150d504f577f596ba88e000bd747f9' + class TestReadWriteText: def test_read_write(self, tmpdir): From 26eaaac4ab32b335b34cf3e6ea5f24e9085a1c60 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:17:36 -0400 Subject: [PATCH 484/835] Add tests for isabs and ismount --- path/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index f9dd6d89..3799e42a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -887,7 +887,12 @@ def read_hexhash(self, hash_name): # bound. Playing it safe and wrapping them all in method calls. def isabs(self): - """.. seealso:: :func:`os.path.isabs`""" + """ + >>> Path('.').isabs() + False + + .. seealso:: :func:`os.path.isabs` + """ return self.module.isabs(self) def exists(self): @@ -907,7 +912,12 @@ def islink(self): return self.module.islink(self) def ismount(self): - """.. seealso:: :func:`os.path.ismount`""" + """ + >>> Path('.').ismount() + False + + .. seealso:: :func:`os.path.ismount` + """ return self.module.ismount(self) def samefile(self, other): From 69edb121d3ae8caf9290433623cb76c53375b529 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:20:27 -0400 Subject: [PATCH 485/835] Implement samefile universally. --- path/__init__.py | 3 --- test_path.py | 1 - 2 files changed, 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 3799e42a..45b4ee79 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -922,9 +922,6 @@ def ismount(self): def samefile(self, other): """.. seealso:: :func:`os.path.samefile`""" - if not hasattr(self.module, 'samefile'): - other = Path(other).realpath().normpath().normcase() - return self.realpath().normpath().normcase() == other return self.module.samefile(self, other) def getatime(self): diff --git a/test_path.py b/test_path.py index 230c4a93..cce96d49 100644 --- a/test_path.py +++ b/test_path.py @@ -734,7 +734,6 @@ def test_chunks(self, tmpdir): assert i == len(txt) / size - 1 - @pytest.mark.skipif(not hasattr(os.path, 'samefile'), reason="samefile not present") def test_samefile(self, tmpdir): f1 = (TempDir() / '1.txt').touch() f1.write_text('foo') From f7e2d5c3e949e4e806d64646445cb33b1e9a22e3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:25:42 -0400 Subject: [PATCH 486/835] Add test for last access time --- path/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 45b4ee79..7baa5351 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -932,7 +932,11 @@ def getatime(self): getatime, None, None, - """ Last access time of the file. + """ + Last access time of the file. + + >>> Path('.').atime > 0 + True .. seealso:: :meth:`getatime`, :func:`os.path.getatime` """, @@ -946,7 +950,8 @@ def getmtime(self): getmtime, None, None, - """ Last-modified time of the file. + """ + Last modified time of the file. .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` """, From 177dbe0201fddc157983c510ebd8ea08fb948cbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:30:25 -0400 Subject: [PATCH 487/835] Add test for access --- path/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 7baa5351..b975b289 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -985,17 +985,16 @@ def getsize(self): """, ) - if hasattr(os, 'access'): - - def access(self, mode): - """Return ``True`` if current user has access to this path. + def access(self, *args, **kwargs): + """ + Return does the real user have access to this path. - mode - One of the constants :data:`os.F_OK`, :data:`os.R_OK`, - :data:`os.W_OK`, :data:`os.X_OK` + >>> Path('.').access(os.F_OK) + True - .. seealso:: :func:`os.access` - """ - return os.access(self, mode) + .. seealso:: :func:`os.access` + """ + return os.access(self, *args, **kwargs) def stat(self): """Perform a ``stat()`` system call on this path. From 9d25a43a4c7dd4095b04b635b35786d0dd2a6c6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:38:04 -0400 Subject: [PATCH 488/835] Add tests for stat and lstat --- path/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index b975b289..8b1bfe20 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -997,14 +997,22 @@ def access(self, *args, **kwargs): return os.access(self, *args, **kwargs) def stat(self): - """Perform a ``stat()`` system call on this path. + """ + Perform a ``stat()`` system call on this path. + + >>> Path('.').stat() + os.stat_result(...) .. seealso:: :meth:`lstat`, :func:`os.stat` """ return os.stat(self) def lstat(self): - """Like :meth:`stat`, but do not follow symbolic links. + """ + Like :meth:`stat`, but do not follow symbolic links. + + >>> Path('.').lstat() == Path('.').stat() + True .. seealso:: :meth:`stat`, :func:`os.lstat` """ From 7575248c54c723ae9d51ccc669e6be1dee7f87cf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:38:58 -0400 Subject: [PATCH 489/835] Add a note in the readme about intentions for API convergence. Fixes #185. --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 86f7df70..834ff5f1 100644 --- a/README.rst +++ b/README.rst @@ -97,6 +97,12 @@ has several advantages over ``pathlib``: subclass the model do not need to be concerned with OS-specific nuances. +This path project has the explicit aim to provide compatibility +with ``pathlib`` objects where possible, such that a ``path.Path`` +object is a drop-in replacement for ``pathlib.Path*`` objects. +This project welcomes contributions to improve that compatibility +where it's lacking. + Alternatives ============ From 6a7af905610d7114fd5457acddd24a5d77c34b77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 11:48:26 -0400 Subject: [PATCH 490/835] Add test for get_owner. Suppress coverage checks. --- path/__init__.py | 23 ++++++++++++----------- setup.cfg | 1 + test_path.py | 5 +++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8b1bfe20..cabc05e4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1018,12 +1018,12 @@ def lstat(self): """ return os.lstat(self) - def __get_owner_windows(self): - """ + def __get_owner_windows(self): # pragma: nocover + r""" Return the name of the owner of this file or directory. Follow symbolic links. - Return a name of the form ``r'DOMAIN\\User Name'``; may be a group. + Return a name of the form ``DOMAIN\User Name``; may be a group. .. seealso:: :attr:`owner` """ @@ -1034,7 +1034,7 @@ def __get_owner_windows(self): account, domain, typecode = win32security.LookupAccountSid(None, sid) return domain + '\\' + account - def __get_owner_unix(self): + def __get_owner_unix(self): # pragma: nocover """ Return the name of the owner of this file or directory. Follow symbolic links. @@ -1044,15 +1044,16 @@ def __get_owner_unix(self): st = self.stat() return pwd.getpwuid(st.st_uid).pw_name - def __get_owner_not_implemented(self): + def __get_owner_not_implemented(self): # pragma: nocover raise NotImplementedError("Ownership not available on this platform.") - if 'win32security' in globals(): - get_owner = __get_owner_windows - elif 'pwd' in globals(): - get_owner = __get_owner_unix - else: - get_owner = __get_owner_not_implemented + get_owner = ( + __get_owner_windows + if 'win32security' in globals() + else __get_owner_unix + if 'pwd' in globals() + else __get_owner_not_implemented + ) owner = property( get_owner, diff --git a/setup.cfg b/setup.cfg index 39edd97e..91b45355 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ testing = # local appdirs packaging + pywin32; platform_system == "Windows" # required for checkdocs on README.rst pygments diff --git a/test_path.py b/test_path.py index cce96d49..67e7f954 100644 --- a/test_path.py +++ b/test_path.py @@ -282,6 +282,11 @@ def test_import_time(self, monkeypatch): assert duration < limit +class TestOwnership: + def test_get_owner(self): + Path('/').get_owner() + + class TestSymbolicLinksWalk: def test_skip_symlinks(self, tmpdir): root = Path(tmpdir) From 6e77c74ca2d799808b7a6754f5634877aae03b70 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 12:52:27 -0400 Subject: [PATCH 491/835] Add tests for statvfs and pathconf --- test_path.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_path.py b/test_path.py index 67e7f954..34be6bfb 100644 --- a/test_path.py +++ b/test_path.py @@ -249,6 +249,14 @@ def test_read_hexhash(self, tmpdir): target.write_text('quick brown fox and lazy dog') assert target.read_hexhash('md5') == '73150d504f577f596ba88e000bd747f9' + @pytest.mark.skipif("not hasattr(os, 'statvfs')") + def test_statvfs(self): + Path('.').statvfs() + + @pytest.mark.skipif("not hasattr(os, 'pathconf')") + def test_pathconf(self): + assert isinstance(Path('.').pathconf(1), int) + class TestReadWriteText: def test_read_write(self, tmpdir): From 2f861b0642f70b18f743b5414ac22ac64ca24965 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 12:58:10 -0400 Subject: [PATCH 492/835] Add test for utime. Allow utime to accept all parameters. --- path/__init__.py | 4 ++-- test_path.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index cabc05e4..05854b0a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1082,12 +1082,12 @@ def pathconf(self, name): # # --- Modifying operations on files and directories - def utime(self, times): + def utime(self, *args, **kwargs): """Set the access and modified times of this file. .. seealso:: :func:`os.utime` """ - os.utime(self, times) + os.utime(self, *args, **kwargs) return self def chmod(self, mode): diff --git a/test_path.py b/test_path.py index 34be6bfb..23570905 100644 --- a/test_path.py +++ b/test_path.py @@ -257,6 +257,12 @@ def test_statvfs(self): def test_pathconf(self): assert isinstance(Path('.').pathconf(1), int) + def test_utime(self, tmpdir): + tmpfile = Path(tmpdir) / 'file' + tmpfile.touch() + new_time = (time.time() - 600,) * 2 + assert Path(tmpfile).utime(new_time).stat().st_atime == new_time[0] + class TestReadWriteText: def test_read_write(self, tmpdir): From c70a5284cc15036d55b32ec48e36c5a873c6ba0e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:13:11 -0400 Subject: [PATCH 493/835] Add test for chmod as a str --- test_path.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test_path.py b/test_path.py index 23570905..9fa0ef15 100644 --- a/test_path.py +++ b/test_path.py @@ -28,6 +28,7 @@ import subprocess import re import contextlib +import stat import pytest @@ -263,6 +264,14 @@ def test_utime(self, tmpdir): new_time = (time.time() - 600,) * 2 assert Path(tmpfile).utime(new_time).stat().st_atime == new_time[0] + def test_chmod_str(self, tmpdir): + tmpfile = Path(tmpdir) / 'file' + tmpfile.touch() + tmpfile.chmod('o-r') + if platform.system() == 'Windows': + return + assert not (tmpfile.stat().st_mode & stat.S_IROTH) + class TestReadWriteText: def test_read_write(self, tmpdir): From 63f7ee89a869342864504090b0f47bc84e35bcb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:13:44 -0400 Subject: [PATCH 494/835] Disable Python 3.10 tests --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7d6b455b..246cd142 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,8 @@ jobs: python: - 3.6 - 3.9 - - 3.10.0-alpha - 3.10.99 + # disabled due to pywin32 availability mhammond/pywin32#1588 + # - 3.10.0-alpha - 3.10.99 platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: From 611ebdd28743e0dd050996ca9b2fe5f1251b050e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:17:15 -0400 Subject: [PATCH 495/835] Hide chown when not available, similar to other methods. --- path/__init__.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 05854b0a..fab0b5e1 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1103,22 +1103,20 @@ def chmod(self, mode): os.chmod(self, mode) return self - def chown(self, uid=-1, gid=-1): - """ - Change the owner and group by names rather than the uid or gid numbers. + if hasattr(os, 'chown'): - .. seealso:: :func:`os.chown` - """ - if hasattr(os, 'chown'): + def chown(self, uid=-1, gid=-1): + """ + Change the owner and group by names rather than the uid or gid numbers. + + .. seealso:: :func:`os.chown` + """ if 'pwd' in globals() and isinstance(uid, str): uid = pwd.getpwnam(uid).pw_uid if 'grp' in globals() and isinstance(gid, str): gid = grp.getgrnam(gid).gr_gid os.chown(self, uid, gid) - else: - msg = "Ownership not available on this platform." - raise NotImplementedError(msg) - return self + return self def rename(self, new): """.. seealso:: :func:`os.rename`""" From 701abad088e1a30dce1223a00e2fcf56b40d7d75 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:17:44 -0400 Subject: [PATCH 496/835] Assume pwd and grp are present if chown is. --- path/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index fab0b5e1..9e9cef94 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1111,9 +1111,9 @@ def chown(self, uid=-1, gid=-1): .. seealso:: :func:`os.chown` """ - if 'pwd' in globals() and isinstance(uid, str): + if isinstance(uid, str): uid = pwd.getpwnam(uid).pw_uid - if 'grp' in globals() and isinstance(gid, str): + if isinstance(gid, str): gid = grp.getgrnam(gid).gr_gid os.chown(self, uid, gid) return self From 0c0a344e8e349ac1686be9b13f21135fd92e1bf7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:25:50 -0400 Subject: [PATCH 497/835] Add test for chown --- path/__init__.py | 15 +++++++++------ test_path.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 9e9cef94..bec3e254 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1107,15 +1107,18 @@ def chmod(self, mode): def chown(self, uid=-1, gid=-1): """ - Change the owner and group by names rather than the uid or gid numbers. + Change the owner and group by names or numbers. .. seealso:: :func:`os.chown` """ - if isinstance(uid, str): - uid = pwd.getpwnam(uid).pw_uid - if isinstance(gid, str): - gid = grp.getgrnam(gid).gr_gid - os.chown(self, uid, gid) + + def resolve_uid(uid): + return uid if isinstance(uid, int) else pwd.getpwnam(uid).pw_uid + + def resolve_gid(gid): + return gid if isinstance(gid, int) else grp.getgrnam(gid).gr_gid + + os.chown(self, resolve_uid(uid), resolve_gid(gid)) return self def rename(self, new): diff --git a/test_path.py b/test_path.py index 9fa0ef15..be39dd9d 100644 --- a/test_path.py +++ b/test_path.py @@ -272,6 +272,16 @@ def test_chmod_str(self, tmpdir): return assert not (tmpfile.stat().st_mode & stat.S_IROTH) + @pytest.mark.skipif("not hasattr(Path, 'chown')") + def test_chown(self, tmpdir): + tmpfile = Path(tmpdir) / 'file' + tmpfile.touch() + tmpfile.chown(os.getuid(), os.getgid()) + import pwd + + name = pwd.getpwuid(os.getuid()).pw_name + tmpfile.chown(name) + class TestReadWriteText: def test_read_write(self, tmpdir): From a312c7f1ce4724dd9ab2c2f0981a886b40b25ece Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:28:11 -0400 Subject: [PATCH 498/835] Add test for renames --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index be39dd9d..52e4baf2 100644 --- a/test_path.py +++ b/test_path.py @@ -282,6 +282,11 @@ def test_chown(self, tmpdir): name = pwd.getpwuid(os.getuid()).pw_name tmpfile.chown(name) + def test_renames(self, tmpdir): + tmpfile = Path(tmpdir) / 'file' + tmpfile.touch() + tmpfile.renames(Path(tmpdir) / 'foo' / 'alt') + class TestReadWriteText: def test_read_write(self, tmpdir): From 2f117e1354c807d5d8def69f3ed059320348e819 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:31:32 -0400 Subject: [PATCH 499/835] Add test for mkdir_p --- test_path.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_path.py b/test_path.py index 52e4baf2..cc28c47c 100644 --- a/test_path.py +++ b/test_path.py @@ -287,6 +287,9 @@ def test_renames(self, tmpdir): tmpfile.touch() tmpfile.renames(Path(tmpdir) / 'foo' / 'alt') + def test_mkdir_p(self, tmpdir): + Path(tmpdir).mkdir_p() + class TestReadWriteText: def test_read_write(self, tmpdir): From 1fba72e41db00718a6f31c13ece2b59abfda04da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:38:18 -0400 Subject: [PATCH 500/835] Add test for removedirs_p --- test_path.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test_path.py b/test_path.py index cc28c47c..989b2bf9 100644 --- a/test_path.py +++ b/test_path.py @@ -290,6 +290,17 @@ def test_renames(self, tmpdir): def test_mkdir_p(self, tmpdir): Path(tmpdir).mkdir_p() + def test_removedirs_p(self, tmpdir): + dir = Path(tmpdir) / 'somedir' + dir.mkdir() + (dir / 'file').touch() + (dir / 'sub').mkdir() + dir.removedirs_p() + assert dir.isdir() + assert (dir / 'file').isfile() + # TODO: shouldn't sub get removed? + # assert not (dir / 'sub').isdir() + class TestReadWriteText: def test_read_write(self, tmpdir): From c4b449b5421c7a155cd706111d454d7887770dda Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:40:02 -0400 Subject: [PATCH 501/835] Remove duplication of remove/unlink. --- path/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index bec3e254..36d79d79 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1208,16 +1208,8 @@ def remove_p(self): self.unlink() return self - def unlink(self): - """.. seealso:: :func:`os.unlink`""" - os.unlink(self) - return self - - def unlink_p(self): - """Like :meth:`unlink`, but does not raise an exception if the - file does not exist.""" - self.remove_p() - return self + unlink = remove + unlink_p = remove_p # --- Links From 85c52d9a48c6195f2cfd0d2cdaf2288b0859878e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:41:48 -0400 Subject: [PATCH 502/835] Define link functions unconditionally. --- path/__init__.py | 70 ++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 36d79d79..c42a3436 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1213,54 +1213,48 @@ def remove_p(self): # --- Links - if hasattr(os, 'link'): + def link(self, newpath): + """Create a hard link at `newpath`, pointing to this file. - def link(self, newpath): - """Create a hard link at `newpath`, pointing to this file. - - .. seealso:: :func:`os.link` - """ - os.link(self, newpath) - return self._next_class(newpath) - - if hasattr(os, 'symlink'): - - def symlink(self, newlink=None): - """Create a symbolic link at `newlink`, pointing here. + .. seealso:: :func:`os.link` + """ + os.link(self, newpath) + return self._next_class(newpath) - If newlink is not supplied, the symbolic link will assume - the name self.basename(), creating the link in the cwd. + def symlink(self, newlink=None): + """Create a symbolic link at `newlink`, pointing here. - .. seealso:: :func:`os.symlink` - """ - if newlink is None: - newlink = self.basename() - os.symlink(self, newlink) - return self._next_class(newlink) + If newlink is not supplied, the symbolic link will assume + the name self.basename(), creating the link in the cwd. - if hasattr(os, 'readlink'): + .. seealso:: :func:`os.symlink` + """ + if newlink is None: + newlink = self.basename() + os.symlink(self, newlink) + return self._next_class(newlink) - def readlink(self): - """Return the path to which this symbolic link points. + def readlink(self): + """Return the path to which this symbolic link points. - The result may be an absolute or a relative path. + The result may be an absolute or a relative path. - .. seealso:: :meth:`readlinkabs`, :func:`os.readlink` - """ - return self._next_class(os.readlink(self)) + .. seealso:: :meth:`readlinkabs`, :func:`os.readlink` + """ + return self._next_class(os.readlink(self)) - def readlinkabs(self): - """Return the path to which this symbolic link points. + def readlinkabs(self): + """Return the path to which this symbolic link points. - The result is always an absolute path. + The result is always an absolute path. - .. seealso:: :meth:`readlink`, :func:`os.readlink` - """ - p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() + .. seealso:: :meth:`readlink`, :func:`os.readlink` + """ + p = self.readlink() + if p.isabs(): + return p + else: + return (self.parent / p).abspath() # High-level functions from shutil # These functions will be bound to the instance such that From a5d846dac017785705fdd8ef81d19c106c17f961 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:48:44 -0400 Subject: [PATCH 503/835] Add test for link --- test_path.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_path.py b/test_path.py index 989b2bf9..bd86477f 100644 --- a/test_path.py +++ b/test_path.py @@ -339,6 +339,14 @@ def test_get_owner(self): Path('/').get_owner() +class TestLinks: + def test_link(self, tmpdir): + target = Path(tmpdir) / 'target' + target.write_text('hello', encoding='utf-8') + link = target.link(Path(tmpdir) / 'link') + assert link.read_text() == 'hello' + + class TestSymbolicLinksWalk: def test_skip_symlinks(self, tmpdir): root = Path(tmpdir) From d574c10d9f3e79aa2618fc9ea19c5cee58223f4a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 13:55:09 -0400 Subject: [PATCH 504/835] Add test for symlink with no parameters --- test_path.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test_path.py b/test_path.py index bd86477f..c5a07d27 100644 --- a/test_path.py +++ b/test_path.py @@ -346,6 +346,13 @@ def test_link(self, tmpdir): link = target.link(Path(tmpdir) / 'link') assert link.read_text() == 'hello' + def test_symlink_none(self, tmpdir): + root = Path(tmpdir) + with root: + file = (Path('dir').mkdir() / 'file').touch() + file.symlink() + assert Path('file').isfile() + class TestSymbolicLinksWalk: def test_skip_symlinks(self, tmpdir): From d3c49896c120c2fcdec92e9143c02db381f4548e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:05:03 -0400 Subject: [PATCH 505/835] Add tests for readlinkabs --- test_path.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test_path.py b/test_path.py index c5a07d27..03e24951 100644 --- a/test_path.py +++ b/test_path.py @@ -353,6 +353,16 @@ def test_symlink_none(self, tmpdir): file.symlink() assert Path('file').isfile() + def test_readlinkabs_passthrough(self, tmpdir): + link = Path(tmpdir) / 'link' + Path('foo').abspath().symlink(link) + link.readlinkabs() == Path('foo').abspath() + + def test_readlinkabs_rendered(self, tmpdir): + link = Path(tmpdir) / 'link' + Path('foo').symlink(link) + link.readlinkabs() == Path(tmpdir) / 'foo' + class TestSymbolicLinksWalk: def test_skip_symlinks(self, tmpdir): From 253baff5060259115f353a2c4a64172b03451888 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:06:23 -0400 Subject: [PATCH 506/835] Collapse branchy return --- path/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index c42a3436..31199a1a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1251,10 +1251,7 @@ def readlinkabs(self): .. seealso:: :meth:`readlink`, :func:`os.readlink` """ p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() + return p if p.isabs() else (self.parent / p).abspath() # High-level functions from shutil # These functions will be bound to the instance such that From a1e398bd4338024f08af4bc94908ffd9d3cdd771 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:15:13 -0400 Subject: [PATCH 507/835] Add tests for chroot and startfile. --- path/__init__.py | 6 +++--- test_path.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 31199a1a..45d055f1 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1332,15 +1332,15 @@ def ignored(item): if hasattr(os, 'chroot'): - def chroot(self): + def chroot(self): # pragma: nocover """.. seealso:: :func:`os.chroot`""" os.chroot(self) if hasattr(os, 'startfile'): - def startfile(self): + def startfile(self, *args, **kwargs): # pragma: nocover """.. seealso:: :func:`os.startfile`""" - os.startfile(self) + os.startfile(self, *args, **kwargs) return self # in-place re-writing, courtesy of Martijn Pieters diff --git a/test_path.py b/test_path.py index 03e24951..ab255bfc 100644 --- a/test_path.py +++ b/test_path.py @@ -417,6 +417,22 @@ def test_touch(self, tmpdir): assert p == ret +@pytest.mark.skipif("not hasattr(Path, 'chroot')") +def test_chroot(monkeypatch): + results = [] + monkeypatch.setattr(os, 'chroot', results.append) + Path().chroot() + assert results == [''] + + +@pytest.mark.skipif("not hasattr(Path, 'startfile')") +def test_startfile(monkeypatch): + results = [] + monkeypatch.setattr(os, 'startfile', results.append) + Path().startfile() + assert results == [''] + + class TestScratchDir: """ Tests that run in a temporary directory (does not test TempDir class) From b6583e6b88625c280f8ef43422895362becf42ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:23:06 -0400 Subject: [PATCH 508/835] Add test for invalid write mode. --- path/__init__.py | 3 +-- test_path.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 45d055f1..92312df4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1410,8 +1410,7 @@ def in_place( ) else: os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC - if hasattr(os, 'O_BINARY'): - os_mode |= os.O_BINARY + os_mode |= getattr(os, 'O_BINARY', 0) fd = os.open(self, os_mode, perm) writable = io.open( fd, diff --git a/test_path.py b/test_path.py index ab255bfc..49ec361f 100644 --- a/test_path.py +++ b/test_path.py @@ -1221,6 +1221,11 @@ def test_exception_in_context(self, tmpdir): assert 'Lorem' not in data assert 'lazy dog' in data + def test_write_mode_invalid(self, tmpdir): + with pytest.raises(ValueError): + with (Path(tmpdir) / 'document').in_place(mode='w'): + pass + class TestSpecialPaths: @pytest.fixture(autouse=True, scope='class') From 1a247cadddf6e29b11f6bfd0398dc0d202266506 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:28:54 -0400 Subject: [PATCH 509/835] Add test for _permission_mask with invalid input. --- path/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index 92312df4..b34ad952 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1657,6 +1657,10 @@ def _permission_mask(mode): >>> _permission_mask('g=')(0o157) == 0o107 True + + >>> _permission_mask('gobbledeegook') + Traceback (most recent call last): + ValueError: ('Unrecognized symbolic mode', 'gobbledeegook') """ # parse the symbolic mode parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) From 286d7de3fd5200b35d54156efdbf3e38eb3d9f63 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:41:19 -0400 Subject: [PATCH 510/835] Add tests for Handlers --- test_path.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test_path.py b/test_path.py index 49ec361f..45e92157 100644 --- a/test_path.py +++ b/test_path.py @@ -1363,3 +1363,30 @@ def test_no_dependencies(): """ cmd = [sys.executable, '-S', '-c', 'import path'] subprocess.check_call(cmd) + + +class TestHandlers: + @staticmethod + def run_with_handler(handler): + try: + raise ValueError() + except Exception: + handler("Something unexpected happened") + + def test_raise(self): + handler = path.Handlers._resolve('strict') + with pytest.raises(ValueError): + self.run_with_handler(handler) + + def test_warn(self): + handler = path.Handlers._resolve('warn') + with pytest.warns(path.TreeWalkWarning): + self.run_with_handler(handler) + + def test_ignore(self): + handler = path.Handlers._resolve('ignore') + self.run_with_handler(handler) + + def test_invalid_handler(self): + with pytest.raises(ValueError): + path.Handlers._resolve('raise') From 17786fea0d926cd538fc53c158a4f7bcd7503baf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:42:51 -0400 Subject: [PATCH 511/835] Exclude compatibility module from coverage testing. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..0fd3adac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + path/py37compat.py [report] show_missing = True From dd5b1a3962a08a136cdfc17de28ef9e2c0c1c0ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:48:43 -0400 Subject: [PATCH 512/835] Improve coverage in test module. --- test_path.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/test_path.py b/test_path.py index 45e92157..f4c83eed 100644 --- a/test_path.py +++ b/test_path.py @@ -74,7 +74,7 @@ def test_relpath(self): cwd = Path(os.getcwd()) assert boz.relpath() == cwd.relpathto(boz) - if os.name == 'nt': + if os.name == 'nt': # pragma: nocover # Check relpath across drives. d = Path('D:\\') assert d.relpathto(boz) == boz @@ -233,7 +233,6 @@ def test_walk_child_error(self, tmpdir): def simulate_access_denied(item): if item.name == 'sub1': raise OSError("Access denied") - return True p = Path(tmpdir) (p / 'sub1').makedirs_p() @@ -268,9 +267,8 @@ def test_chmod_str(self, tmpdir): tmpfile = Path(tmpdir) / 'file' tmpfile.touch() tmpfile.chmod('o-r') - if platform.system() == 'Windows': - return - assert not (tmpfile.stat().st_mode & stat.S_IROTH) + is_windows = platform.system() == 'Windows' + assert is_windows or not (tmpfile.stat().st_mode & stat.S_IROTH) @pytest.mark.skipif("not hasattr(Path, 'chown')") def test_chown(self, tmpdir): @@ -490,7 +488,7 @@ def test_touch(self, tmpdir): assert t2 <= f.mtime <= t3 if hasattr(os.path, 'getctime'): ct2 = f.ctime - if os.name == 'nt': + if platform.system() == 'Windows': # pragma: nocover # On Windows, "ctime" is CREATION time assert ct == ct2 assert ct2 < t2 @@ -552,7 +550,7 @@ def test_listing(self, tmpdir): platform.system() != "Linux", reason="Only Linux allows writing invalid encodings", ) - def test_listdir_other_encoding(self, tmpdir): + def test_listdir_other_encoding(self, tmpdir): # pragma: nocover """ Some filesystems allow non-character sequences in path names. ``.listdir`` should still function in this case. @@ -657,10 +655,7 @@ def test_shutil(self, tmpdir): assert testFile.bytes() == testCopy2.bytes() # Make a link for the next test to use. - if hasattr(os, 'symlink'): - testFile.symlink(testLink) - else: - testFile.copy(testLink) # fallback + testFile.symlink(testLink) # Test copying directory tree. testA.copytree(testC) @@ -909,10 +904,7 @@ def testing_structure(self, tmpdir): with open(self.test_file, 'w') as f: f.write('x' * 10000) - if hasattr(os, 'symlink'): - self.test_file.symlink(self.test_link) - else: - self.test_file.copy(self.test_link) + self.test_file.symlink(self.test_link) def check_link(self): target = Path(self.subdir_b / self.test_link.name) From ad9175d0a473835017cd7434bb7f59d3e15e692b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:50:26 -0400 Subject: [PATCH 513/835] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a2bc6c1e..8f384eb2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v16.1.0 +------- + +- #204: Improved test coverage across the package to 99%, fixing + bugs in uncovered code along the way. + v16.0.0 ------- From a4146c73318f379d7e4d5a049a1790c57e27c393 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 14:55:46 -0400 Subject: [PATCH 514/835] Clean up matchers --- path/matchers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/matchers.py b/path/matchers.py index 053f25a8..63ca218a 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -4,7 +4,7 @@ def load(param): """ - If the supplied parameter is a string, assum it's a simple + If the supplied parameter is a string, assume it's a simple pattern. """ return ( @@ -16,7 +16,7 @@ def load(param): ) -class Base(object): +class Base: pass From 44787990d7ccd16748a1a1cb563660a0772d370b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 17:47:53 -0400 Subject: [PATCH 515/835] Remove extraneous import --- path/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index b34ad952..1e1ead23 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1380,8 +1380,6 @@ def in_place( Thereafter, the file at `filename` will have line numbers in it. """ - import io - if set(mode).intersection('wa+'): raise ValueError('Only read-only file modes can be used') From fe7b4e739bbea2da03441c5463fdbcf21539c26d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 17:54:41 -0400 Subject: [PATCH 516/835] Move 'masks' functionility into its own module. --- path/__init__.py | 86 ++---------------------------------------------- path/masks.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 path/masks.py diff --git a/path/__init__.py b/path/__init__.py index 1e1ead23..f4794ad3 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -32,7 +32,6 @@ import errno import tempfile import functools -import operator import re import contextlib import io @@ -49,6 +48,7 @@ import grp from . import matchers +from . import masks from .py37compat import best_realpath, lru_cache @@ -67,12 +67,6 @@ class TreeWalkWarning(Warning): pass -# from jaraco.functools -def compose(*funcs): - compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa - return functools.reduce(compose_two, funcs) - - class ClassProperty(property): def __get__(self, cls, owner): return self.fget.__get__(None, owner)() @@ -1098,7 +1092,7 @@ def chmod(self, mode): .. seealso:: :func:`os.chmod` """ if isinstance(mode, str): - mask = _multi_permission_mask(mode) + mask = masks.compound(mode) mode = mask(self.stat().st_mode) os.chmod(self, mode) return self @@ -1619,82 +1613,6 @@ def __exit__(self, exc_type, exc_value, traceback): self.rmtree() -def _multi_permission_mask(mode): - """ - Support multiple, comma-separated Unix chmod symbolic modes. - - >>> oct(_multi_permission_mask('a=r,u+w')(0)) - '0o644' - """ - return compose(*map(_permission_mask, reversed(mode.split(',')))) - - -def _permission_mask(mode): - """ - Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function - suitable for applying to a mask to affect that change. - - >>> mask = _permission_mask('ugo+rwx') - >>> mask(0o554) == 0o777 - True - - >>> _permission_mask('go-x')(0o777) == 0o766 - True - - >>> _permission_mask('o-x')(0o445) == 0o444 - True - - >>> _permission_mask('a+x')(0) == 0o111 - True - - >>> _permission_mask('a=rw')(0o057) == 0o666 - True - - >>> _permission_mask('u=x')(0o666) == 0o166 - True - - >>> _permission_mask('g=')(0o157) == 0o107 - True - - >>> _permission_mask('gobbledeegook') - Traceback (most recent call last): - ValueError: ('Unrecognized symbolic mode', 'gobbledeegook') - """ - # parse the symbolic mode - parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) - if not parsed: - raise ValueError("Unrecognized symbolic mode", mode) - - # generate a mask representing the specified permission - spec_map = dict(r=4, w=2, x=1) - specs = (spec_map[perm] for perm in parsed.group('what')) - spec = functools.reduce(operator.or_, specs, 0) - - # now apply spec to each subject in who - shift_map = dict(u=6, g=3, o=0) - who = parsed.group('who').replace('a', 'ugo') - masks = (spec << shift_map[subj] for subj in who) - mask = functools.reduce(operator.or_, masks) - - op = parsed.group('op') - - # if op is -, invert the mask - if op == '-': - mask ^= 0o777 - - # if op is =, retain extant values for unreferenced subjects - if op == '=': - masks = (0o7 << shift_map[subj] for subj in who) - retain = functools.reduce(operator.or_, masks) ^ 0o777 - - op_map = { - '+': operator.or_, - '-': operator.and_, - '=': lambda mask, target: target & retain ^ mask, - } - return functools.partial(op_map[op], mask) - - class Handlers: def strict(msg): raise diff --git a/path/masks.py b/path/masks.py new file mode 100644 index 00000000..761e51f8 --- /dev/null +++ b/path/masks.py @@ -0,0 +1,85 @@ +import re +import functools +import operator + + +# from jaraco.functools +def compose(*funcs): + compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa + return functools.reduce(compose_two, funcs) + + +def compound(mode): + """ + Support multiple, comma-separated Unix chmod symbolic modes. + + >>> oct(compound('a=r,u+w')(0)) + '0o644' + """ + return compose(*map(simple, reversed(mode.split(',')))) + + +def simple(mode): + """ + Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function + suitable for applying to a mask to affect that change. + + >>> mask = simple('ugo+rwx') + >>> mask(0o554) == 0o777 + True + + >>> simple('go-x')(0o777) == 0o766 + True + + >>> simple('o-x')(0o445) == 0o444 + True + + >>> simple('a+x')(0) == 0o111 + True + + >>> simple('a=rw')(0o057) == 0o666 + True + + >>> simple('u=x')(0o666) == 0o166 + True + + >>> simple('g=')(0o157) == 0o107 + True + + >>> simple('gobbledeegook') + Traceback (most recent call last): + ValueError: ('Unrecognized symbolic mode', 'gobbledeegook') + """ + # parse the symbolic mode + parsed = re.match('(?P[ugoa]+)(?P[-+=])(?P[rwx]*)$', mode) + if not parsed: + raise ValueError("Unrecognized symbolic mode", mode) + + # generate a mask representing the specified permission + spec_map = dict(r=4, w=2, x=1) + specs = (spec_map[perm] for perm in parsed.group('what')) + spec = functools.reduce(operator.or_, specs, 0) + + # now apply spec to each subject in who + shift_map = dict(u=6, g=3, o=0) + who = parsed.group('who').replace('a', 'ugo') + masks = (spec << shift_map[subj] for subj in who) + mask = functools.reduce(operator.or_, masks) + + op = parsed.group('op') + + # if op is -, invert the mask + if op == '-': + mask ^= 0o777 + + # if op is =, retain extant values for unreferenced subjects + if op == '=': + masks = (0o7 << shift_map[subj] for subj in who) + retain = functools.reduce(operator.or_, masks) ^ 0o777 + + op_map = { + '+': operator.or_, + '-': operator.and_, + '=': lambda mask, target: target & retain ^ mask, + } + return functools.partial(op_map[op], mask) From 0988c232ba87820bbe5917898dbef55eb624cdf5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:06:50 -0400 Subject: [PATCH 517/835] Remove isinstance check as it's superfluous. --- path/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index f4794ad3..d1c0d35b 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -394,9 +394,7 @@ def joinpath(cls, first, *others): .. seealso:: :func:`os.path.join` """ - if not isinstance(first, cls): - first = cls(first) - return first._next_class(first.module.join(first, *others)) + return cls._next_class(cls.module.join(first, *others)) def splitall(self): r"""Return a list of the path components in this path. From e77f3aa8abfd08e93f4cd7dd2cd13998199a5665 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:19:59 -0400 Subject: [PATCH 518/835] Move ClassProperty and multimethod to their own classes module. --- path/__init__.py | 33 ++++++--------------------------- path/classes.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 path/classes.py diff --git a/path/__init__.py b/path/__init__.py index d1c0d35b..1a2d873c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -49,6 +49,7 @@ from . import matchers from . import masks +from . import classes from .py37compat import best_realpath, lru_cache @@ -67,28 +68,6 @@ class TreeWalkWarning(Warning): pass -class ClassProperty(property): - def __get__(self, cls, owner): - return self.fget.__get__(None, owner)() - - -class multimethod: - """ - Acts like a classmethod when invoked from the class and like an - instancemethod when invoked from the instance. - """ - - def __init__(self, func): - self.func = func - - def __get__(self, instance, owner): - return ( - functools.partial(self.func, owner) - if instance is None - else functools.partial(self.func, owner, instance) - ) - - class Traversal: """ Wrap a walk result to customize the traversal. @@ -161,7 +140,7 @@ def using_module(cls, module): ns = {'module': module} return type(subclass_name, bases, ns) - @ClassProperty + @classes.ClassProperty @classmethod def _next_class(cls): """ @@ -384,7 +363,7 @@ def stripext(self): """ return self.splitext()[0] - @multimethod + @classes.multimethod def joinpath(cls, first, *others): """ Join first to zero or more :class:`Path` components, @@ -1427,7 +1406,7 @@ def in_place( finally: backup_fn.remove_p() - @ClassProperty + @classes.ClassProperty @classmethod def special(cls): """ @@ -1559,7 +1538,7 @@ def detect(cls, input): def __iter__(self): return iter(map(self._next_class, self.split(os.pathsep))) - @ClassProperty + @classes.ClassProperty @classmethod def _next_class(cls): """ @@ -1588,7 +1567,7 @@ class TempDir(Path): .. seealso:: :func:`tempfile.mkdtemp` """ - @ClassProperty + @classes.ClassProperty @classmethod def _next_class(cls): return Path diff --git a/path/classes.py b/path/classes.py new file mode 100644 index 00000000..b6101d0a --- /dev/null +++ b/path/classes.py @@ -0,0 +1,27 @@ +import functools + + +class ClassProperty(property): + def __get__(self, cls, owner): + return self.fget.__get__(None, owner)() + + +class multimethod: + """ + Acts like a classmethod when invoked from the class and like an + instancemethod when invoked from the instance. + """ + + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + """ + If called on an instance, pass the instance as the first + argument. + """ + return ( + functools.partial(self.func, owner) + if instance is None + else functools.partial(self.func, owner, instance) + ) From f0a2f1f188d7287c76afc5a9ac45b550f77b87da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:36:22 -0400 Subject: [PATCH 519/835] Reword write_text docstring --- path/__init__.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1a2d873c..60c3b948 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -682,24 +682,22 @@ def write_text( Parameters: - `text` - str/unicode - The text to be written. + `text` - str/bytes - The text to be written. - `encoding` - str - The Unicode encoding that will be used. - This is ignored if `text` isn't a Unicode string. + `encoding` - str - The text encoding used. `errors` - str - How to handle Unicode encoding errors. Default is ``'strict'``. See ``help(unicode.encode)`` for the - options. This is ignored if `text` isn't a Unicode - string. + options. Ignored if `text` isn't a Unicode string. `linesep` - keyword argument - str/unicode - The sequence of characters to be used to mark end-of-line. The default is - :data:`os.linesep`. You can also specify ``None`` to - leave all newlines as they are in `text`. + :data:`os.linesep`. Specify ``None`` to + use newlines unmodified. `append` - keyword argument - bool - Specifies what to do if the file already exists (``True``: append to the end of it; - ``False``: overwrite it.) The default is ``False``. + ``False``: overwrite it). The default is ``False``. --- Newline handling. @@ -709,18 +707,13 @@ def write_text( end-of-line sequence (see :data:`os.linesep`; on Windows, for example, the end-of-line marker is ``'\r\n'``). - If you don't like your platform's default, you can override it - using the `linesep=` keyword argument. If you specifically want - ``write_text()`` to preserve the newlines as-is, use ``linesep=None``. - - This applies to Unicode text the same as to 8-bit text, except - there are three additional standard Unicode end-of-line sequences: - ``u'\x85'``, ``u'\r\x85'``, and ``u'\u2028'``. - - (This is slightly different from when you open a file for - writing with ``fopen(filename, "w")`` in C or ``open(filename, 'w')`` - in Python.) + To override the platform's default, pass the `linesep=` + keyword argument. To preserve the newlines as-is, pass + ``linesep=None``. + This handling applies to Unicode text and bytes, except + with Unicode, additional non-ASCII newlines are recognized: + ``\x85``, ``\r\x85``, and ``\u2028``. --- Unicode @@ -732,16 +725,15 @@ def write_text( specified `encoding` (or the default encoding if `encoding` isn't specified). The `errors` argument applies only to this conversion. - """ if isinstance(text, str): if linesep is not None: text = U_NEWLINE.sub(linesep, text) - text = text.encode(encoding or sys.getdefaultencoding(), errors) + bytes = text.encode(encoding or sys.getdefaultencoding(), errors) else: assert encoding is None - text = B_NEWLINE.sub(linesep.encode(), text) - self.write_bytes(text, append=append) + bytes = B_NEWLINE.sub(linesep.encode(), text) + self.write_bytes(bytes, append=append) def lines(self, encoding=None, errors='strict', retain=True): r"""Open this file, read all lines, return them in a list. From 1badc1431cbf8b4065ef8e59c68e93a99ace7447 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:48:40 -0400 Subject: [PATCH 520/835] Fix logic that linesep is not checked when bytes are passed. --- path/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 60c3b948..dbd32227 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -732,7 +732,9 @@ def write_text( bytes = text.encode(encoding or sys.getdefaultencoding(), errors) else: assert encoding is None - bytes = B_NEWLINE.sub(linesep.encode(), text) + if linesep is not None: + text = B_NEWLINE.sub(linesep.encode(), text) + bytes = text self.write_bytes(bytes, append=append) def lines(self, encoding=None, errors='strict', retain=True): From a99d6ec53859ba1310eebd31957152d239a63513 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:54:10 -0400 Subject: [PATCH 521/835] Collapse write_bytes --- path/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index dbd32227..6acdcdeb 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -638,11 +638,7 @@ def write_bytes(self, bytes, append=False): Default behavior is to overwrite any existing file. Call ``p.write_bytes(bytes, append=True)`` to append instead. """ - if append: - mode = 'ab' - else: - mode = 'wb' - with self.open(mode) as f: + with self.open('ab' if append else 'wb') as f: f.write(bytes) def read_text(self, encoding=None, errors=None): From db96a8e4b4b709fccc01dd58c1c946f26011dae5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 18:59:44 -0400 Subject: [PATCH 522/835] Deprecate writing bytes using write_text. --- CHANGES.rst | 6 ++++++ path/__init__.py | 5 +++++ test_path.py | 1 + 3 files changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8f384eb2..30daf94f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v16.2.0 +------- + +- Deprecated passing bytes to ``write_text``. Instead, users + should call ``write_bytes``. + v16.1.0 ------- diff --git a/path/__init__.py b/path/__init__.py index 6acdcdeb..83ede4cb 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -727,6 +727,11 @@ def write_text( text = U_NEWLINE.sub(linesep, text) bytes = text.encode(encoding or sys.getdefaultencoding(), errors) else: + warnings.warn( + "Writing bytes in write_text is deprecated", + DeprecationWarning, + stacklevel=1, + ) assert encoding is None if linesep is not None: text = B_NEWLINE.sub(linesep.encode(), text) diff --git a/test_path.py b/test_path.py index f4c83eed..c18e9a07 100644 --- a/test_path.py +++ b/test_path.py @@ -301,6 +301,7 @@ def test_removedirs_p(self, tmpdir): class TestReadWriteText: + @pytest.mark.filterwarnings('ignore:Writing bytes in write_text') def test_read_write(self, tmpdir): file = path.Path(tmpdir) / 'filename' file.write_text('hello world') From 750666222dea3929b5068ee9e062a3e7b84b8d90 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 19:13:32 -0400 Subject: [PATCH 523/835] Reword lines docstring --- path/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 83ede4cb..84cff469 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -738,20 +738,20 @@ def write_text( bytes = text self.write_bytes(bytes, append=append) - def lines(self, encoding=None, errors='strict', retain=True): + def lines(self, encoding=None, errors=None, retain=True): r"""Open this file, read all lines, return them in a list. Optional arguments: `encoding` - The Unicode encoding (or character set) of - the file. The default is ``None``, meaning the content - of the file is read as 8-bit characters and returned - as a list of (non-Unicode) str objects. - `errors` - How to handle Unicode errors; see help(str.decode) - for the options. Default is ``'strict'``. - `retain` - If ``True``, retain newline characters; but all newline - character combinations (``'\r'``, ``'\n'``, ``'\r\n'``) are - translated to ``'\n'``. If ``False``, newline characters are - stripped off. Default is ``True``. + the file. The default is ``None``, meaning use + ``locale.getpreferredencoding()``. + `errors` - How to handle Unicode errors; see + `open `_ + for the options. Default is ``None`` meaning "strict". + `retain` - If ``True`` (default), retain newline characters, + but translate all newline + characters to ``\n``. If ``False``, newline characters are + omitted. .. seealso:: :meth:`text` """ From 983f8254e7767a3b13f54633157d7f24ad106649 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 19:22:45 -0400 Subject: [PATCH 524/835] Simplify patterns for newline handling. --- path/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 84cff469..63769030 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -58,10 +58,10 @@ LINESEPS = ['\r\n', '\r', '\n'] U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] -U_NEWLINE = re.compile('|'.join(U_LINESEPS)) B_NEWLINE = re.compile('|'.join(LINESEPS).encode()) -B_NL_END = re.compile(f'(?:{B_NEWLINE.pattern.decode()})$'.encode()) -U_NL_END = re.compile(f'(?:{U_NEWLINE.pattern})$') +U_NEWLINE = re.compile('|'.join(U_LINESEPS)) +B_NL_END = re.compile(B_NEWLINE.pattern + b'$') +U_NL_END = re.compile(U_NEWLINE.pattern + '$') class TreeWalkWarning(Warning): From 0890c09cb494eb4142e67d0c845e1ee11b13c761 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 19:19:38 -0400 Subject: [PATCH 525/835] Rely on 'open' method to set up encoding for writing lines. --- path/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 63769030..0b77c72a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -797,14 +797,13 @@ def write_lines( mixed-encoding data, which can really confuse someone trying to read the file later. """ - with self.open('ab' if append else 'wb') as f: + mode = 'a' if append else 'w' + with self.open(mode, encoding=encoding, errors=errors) as f: for line in lines: - isUnicode = isinstance(line, str) + is_unicode = isinstance(line, str) if linesep is not None: - pattern = U_NL_END if isUnicode else B_NL_END + pattern = U_NL_END if is_unicode else B_NL_END line = pattern.sub('', line) + linesep - if isUnicode: - line = line.encode(encoding or sys.getdefaultencoding(), errors) f.write(line) def read_md5(self): From e0736d80125a6e804d4bcfbe6ba7df0bb4b87ef5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jul 2021 19:26:13 -0400 Subject: [PATCH 526/835] Inline is_unicode --- path/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 0b77c72a..5e2193cf 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -800,9 +800,8 @@ def write_lines( mode = 'a' if append else 'w' with self.open(mode, encoding=encoding, errors=errors) as f: for line in lines: - is_unicode = isinstance(line, str) if linesep is not None: - pattern = U_NL_END if is_unicode else B_NL_END + pattern = U_NL_END if isinstance(line, str) else B_NL_END line = pattern.sub('', line) + linesep f.write(line) From a76a548d0f25947d2594d36b784d029a6ada977f Mon Sep 17 00:00:00 2001 From: Alan Fregtman <941331+darkvertex@users.noreply.github.com> Date: Mon, 26 Jul 2021 10:55:08 -0400 Subject: [PATCH 527/835] .editorconfig: Set max_line_length to 88 for Python files. --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 6385b573..b8aeea17 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,7 @@ end_of_line = lf [*.py] indent_style = space +max_line_length = 88 [*.{yml,yaml}] indent_style = space From 8ea55f2fb26bd77997f0e9435bab2d41376a76d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Sep 2021 21:23:38 -0400 Subject: [PATCH 528/835] Add intersphinx mappings for Python to prevent spurious nitpicky failures. Fixes jaraco/skeleton#51. --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index f65d1faa..4ae74093 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,3 +27,10 @@ # Be strict about any broken references: nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} From dc43378c8accd85321b42e3fe69fcb87e5266006 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 5 Oct 2021 22:45:21 -0400 Subject: [PATCH 529/835] Test on Python 3.10 (final). --- .github/workflows/main.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7d6b455b..6aad7f11 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,8 +9,11 @@ jobs: python: - 3.6 - 3.9 - - 3.10.0-alpha - 3.10.99 - platform: [ubuntu-latest, macos-latest, windows-latest] + - "3.10" + platform: + - ubuntu-latest + - macos-latest + - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 @@ -34,7 +37,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Install tox run: | python -m pip install tox From 5823e9ca9d242b733a5ff3c8e2c22e13ec0a4c01 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 7 Oct 2021 19:52:04 -0400 Subject: [PATCH 530/835] Rely on pytest 6 and drop workaround for pytest-dev/pytest#6178. --- pytest.ini | 2 -- setup.cfg | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 31b114fd..9ecdba49 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,8 +2,6 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS -# workaround for warning pytest-dev/pytest#6178 -junit_family=xunit2 filterwarnings= # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 diff --git a/setup.cfg b/setup.cfg index 69eb0ee6..0f7d652d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ exclude = [options.extras_require] testing = # upstream - pytest >= 4.6 + pytest >= 6 pytest-checkdocs >= 2.4 pytest-flake8 pytest-black >= 0.3.7; \ From aae281a9ff6c9a1fa9daad82c79457e8770a1c7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Oct 2021 14:19:58 -0400 Subject: [PATCH 531/835] Remove wheel from build requirements. It's implied for wheel builds. Ref pypa/setuptools#1498. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 28bd7883..190b3551 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=56", "wheel", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] From 0019b0af43b9e381e2f0b14753d1bf40ce204490 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 20:08:49 -0500 Subject: [PATCH 532/835] Require Python 3.7 or later. --- .github/workflows/main.yml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6aad7f11..5424298d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: python: - - 3.6 + - 3.7 - 3.9 - "3.10" platform: diff --git a/setup.cfg b/setup.cfg index 0f7d652d..bd1da7a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = [options] packages = find_namespace: include_package_data = true -python_requires = >=3.6 +python_requires = >=3.7 install_requires = [options.packages.find] From eca1c4ca6e104c8add280c721cbb365196f55ac7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 20:38:46 -0500 Subject: [PATCH 533/835] Remove filtered warnings, addressed upstream. --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 9ecdba49..ec965b24 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,5 +5,3 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 - # Suppress deprecation warning in pypa/packaging#433 - ignore:The distutils package is deprecated::packaging.tags From 94b8dc3e3923e8594dd41b783d4918db545e5fb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 19:56:06 -0500 Subject: [PATCH 534/835] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 30daf94f..1ffd459c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v16.3.0 +------- + +Require Python 3.7 or later. + v16.2.0 ------- From 88f4edb4238ce8ab0ff53b4dd06a8f81138c47ef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 19:56:48 -0500 Subject: [PATCH 535/835] Enable testing on Python 3.10 --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f03657fc..5424298d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,8 +9,7 @@ jobs: python: - 3.7 - 3.9 - # disabled due to pywin32 availability mhammond/pywin32#1588 - # - "3.10" + - "3.10" platform: - ubuntu-latest - macos-latest From f65698c51c95767bfa29e8b4cc55d363621d1888 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 20:13:18 -0500 Subject: [PATCH 536/835] Remove macOS handling, which hasn't been supported for some time. --- test_path.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test_path.py b/test_path.py index c18e9a07..64361fcb 100644 --- a/test_path.py +++ b/test_path.py @@ -572,10 +572,6 @@ def test_listdir_other_encoding(self, tmpdir): # pragma: nocover assert len(results) == 1 (res,) = results assert isinstance(res, Path) - # OS X seems to encode the bytes in the filename as %XX characters. - if platform.system() == 'Darwin': - assert res.basename() == 'r%E9%F1emi' - return assert len(res.basename()) == len(filename) def test_makedirs(self, tmpdir): From a484dd7731f5e0c60b00e3e72efb9859d7de9671 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 20:55:02 -0500 Subject: [PATCH 537/835] Refactor test_listdir_other_encoding to skip the test when the test file cannot be written. Fixes #205. --- test_path.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test_path.py b/test_path.py index 64361fcb..89b73b18 100644 --- a/test_path.py +++ b/test_path.py @@ -547,32 +547,31 @@ def test_listing(self, tmpdir): with contextlib.suppress(Exception): f.remove() - @pytest.mark.skipif( - platform.system() != "Linux", - reason="Only Linux allows writing invalid encodings", - ) - def test_listdir_other_encoding(self, tmpdir): # pragma: nocover + @pytest.fixture + def bytes_filename(self, tmpdir): + name = r'r\xe9\xf1emi'.encode('latin-1') + base = str(tmpdir).encode('ascii') + try: + with open(os.path.join(base, name), 'wb'): + pass + except Exception as exc: + raise pytest.skip(f"Invalid encodings disallowed {exc}") + return name + + def test_listdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover """ Some filesystems allow non-character sequences in path names. ``.listdir`` should still function in this case. See issue #61 for details. """ - assert Path(tmpdir).listdir() == [] - tmpdir_bytes = str(tmpdir).encode('ascii') - - filename = 'r\xe9\xf1emi'.encode('latin-1') - pathname = os.path.join(tmpdir_bytes, filename) - with open(pathname, 'wb'): - pass # first demonstrate that os.listdir works - assert os.listdir(tmpdir_bytes) + assert os.listdir(str(tmpdir).encode('ascii')) # now try with path results = Path(tmpdir).listdir() - assert len(results) == 1 (res,) = results assert isinstance(res, Path) - assert len(res.basename()) == len(filename) + assert len(res.basename()) == len(bytes_filename) def test_makedirs(self, tmpdir): d = Path(tmpdir) From 0dcb5db513d47e6b62cf11bf7f0234d84abef2fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 21:00:19 -0500 Subject: [PATCH 538/835] Update changelog --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1ffd459c..b417e9dc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,9 @@ v16.3.0 ------- -Require Python 3.7 or later. +- Require Python 3.7 or later. +- #205: test_listdir_other_encoding now automatically skips + itself on file systems where it's not appropriate. v16.2.0 ------- From 4f9825dafa8d13a5f8b8bd8eb8bfc6414329cb18 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 04:09:17 -0500 Subject: [PATCH 539/835] Update badge year --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a3e1b740..c82c6429 100644 --- a/README.rst +++ b/README.rst @@ -17,5 +17,5 @@ .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2021-informational +.. image:: https://img.shields.io/badge/skeleton-2022-informational :target: https://blog.jaraco.com/skeleton From 7e01b721c237ee4947e3b9d6e56bb03a028f3f6a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 04:11:54 -0500 Subject: [PATCH 540/835] Remove setup.py, no longer needed. --- setup.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index bac24a43..00000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -import setuptools - -if __name__ == "__main__": - setuptools.setup() From 7d2ffc0843c68b185438a7a0bd8bf06068f0e6ad Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 31 Jan 2022 22:34:46 -0800 Subject: [PATCH 541/835] Type hint path library Type hints have been added via stub files. --- path/__init__.pyi | 471 ++++++++++++++++++++++++++++++++++++++++++++ path/classes.pyi | 8 + path/masks.pyi | 5 + path/matchers.pyi | 25 +++ path/py37compat.pyi | 17 ++ 5 files changed, 526 insertions(+) create mode 100644 path/__init__.pyi create mode 100644 path/classes.pyi create mode 100644 path/masks.pyi create mode 100644 path/matchers.pyi create mode 100644 path/py37compat.pyi diff --git a/path/__init__.pyi b/path/__init__.pyi new file mode 100644 index 00000000..8657aea9 --- /dev/null +++ b/path/__init__.pyi @@ -0,0 +1,471 @@ +from __future__ import annotations + +import builtins +import contextlib +import os +from _typeshed import ( + OpenBinaryMode, + OpenBinaryModeUpdating, + OpenBinaryModeReading, + OpenBinaryModeWriting, + OpenTextMode, + Self, +) +from io import ( + BufferedRandom, + BufferedReader, + BufferedWriter, + FileIO, + TextIOWrapper, +) +from types import ModuleType, TracebackType +from typing import ( + Any, + AnyStr, + BinaryIO, + Callable, + Generator, + Iterable, + Iterator, + IO, + List, + Literal, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from . import classes + +# Type for the match argument for several methods +_Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] + +class TreeWalkWarning(Warning): + pass + +class Traversal: + follow: Callable[[Path], bool] + def __init__(self, follow: Callable[[Path], bool]): ... + def __call__( + self, + walker: Generator[Path, Optional[Callable[[], bool]], None], + ) -> Iterator[Path]: ... + +class Path(str): + module: Any + def __init__(self, other: Any = ...) -> None: ... + @classmethod + def using_module(cls, module: ModuleType) -> Type[Path]: ... + @classes.ClassProperty + @classmethod + def _next_class(cls: Type[Self]) -> Type[Self]: ... + def __repr__(self) -> str: ... + def __add__(self: Self, more: str) -> Self: ... + def __radd__(self: Self, other: str) -> Self: ... + def __div__(self: Self, rel: str) -> Self: ... + def __truediv__(self: Self, rel: str) -> Self: ... + def __rdiv__(self: Self, rel: str) -> Self: ... + def __rtruediv__(self: Self, rel: str) -> Self: ... + def __enter__(self: Self) -> Self: ... + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: ... + @classmethod + def getcwd(cls: Type[Self]) -> Self: ... + def abspath(self: Self) -> Self: ... + def normcase(self: Self) -> Self: ... + def normpath(self: Self) -> Self: ... + def realpath(self: Self) -> Self: ... + def expanduser(self: Self) -> Self: ... + def expandvars(self: Self) -> Self: ... + def dirname(self: Self) -> Self: ... + def basename(self: Self) -> Self: ... + def expand(self: Self) -> Self: ... + @property + def stem(self) -> str: ... + @property + def ext(self) -> str: ... + def with_suffix(self: Self, suffix: str) -> Self: ... + @property + def drive(self: Self) -> Self: ... + @property + def parent(self: Self) -> Self: ... + @property + def name(self: Self) -> Self: ... + def splitpath(self: Self) -> Tuple[Self, str]: ... + def splitdrive(self: Self) -> Tuple[Self, Self]: ... + def splitext(self: Self) -> Tuple[Self, str]: ... + def stripext(self: Self) -> Self: ... + @classes.multimethod + def joinpath(cls: Self, first: str, *others: str) -> Self: ... + def splitall(self: Self) -> List[Union[Self, str]]: ... + def parts(self: Self) -> Tuple[Union[Self, str], ...]: ... + def _parts(self: Self) -> Iterator[Union[Self, str]]: ... + def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: ... + def relpath(self: Self, start: str = ...) -> Self: ... + def relpathto(self: Self, dest: str) -> Self: ... + # --- Listing, searching, walking, and matching + def listdir(self: Self, match: _Match = ...) -> List[Self]: ... + def dirs(self: Self, match: _Match = ...) -> List[Self]: ... + def files(self: Self, match: _Match = ...) -> List[Self]: ... + def walk( + self: Self, + match: _Match = ..., + errors: str = ..., + ) -> Generator[Self, Optional[Callable[[], bool]], None]: ... + def walkdirs( + self: Self, + match: _Match = ..., + errors: str = ..., + ) -> Iterator[Self]: ... + def walkfiles( + self: Self, + match: _Match = ..., + errors: str = ..., + ) -> Iterator[Self]: ... + def fnmatch( + self, + pattern: Union[Path, str], + normcase: Optional[Callable[[str], str]] = ..., + ) -> bool: ... + def glob(self: Self, pattern: str) -> List[Self]: ... + def iglob(self: Self, pattern: str) -> Iterator[Self]: ... + @overload + def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> TextIOWrapper: ... + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> FileIO: ... + @overload + def open( + self, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedRandom: ... + @overload + def open( + self, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedReader: ... + @overload + def open( + self, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedWriter: ... + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: int, + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BinaryIO: ... + @overload + def open( + self, + mode: str, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> IO[Any]: ... + def bytes(self) -> builtins.bytes: ... + @overload + def chunks( + self, + size: int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[str]: ... + @overload + def chunks( + self, + size: int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[builtins.bytes]: ... + @overload + def chunks( + self, + size: int, + mode: str, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[Union[str, builtins.bytes]]: ... + def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... + def read_text( + self, encoding: Optional[str] = ..., errors: Optional[str] = ... + ) -> str: ... + def read_bytes(self) -> builtins.bytes: ... + def text(self, encoding: Optional[str]=..., errors: str=...) -> str: ... + @overload + def write_text( + self, + text: str, + encoding: Optional[str] = ..., + errors: str = ..., + linesep: Optional[str] = ..., + append: bool = ..., + ) -> None: ... + @overload + def write_text( + self, + text: builtins.bytes, + encoding: None = ..., + errors: str = ..., + linesep: Optional[str] = ..., + append: bool = ..., + ) -> None: ... + def lines( + self, + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + retain: bool = ..., + ) -> List[str]: ... + def write_lines( + self, + lines: List[str], + encoding: Optional[str] = ..., + errors: str = ..., + linesep: Optional[str] = ..., + append: bool = ..., + ) -> None: ... + def read_md5(self) -> builtins.bytes: ... + def read_hash(self, hash_name: str) -> builtins.bytes: ... + def read_hexhash(self, hash_name: str) -> str: ... + def isabs(self) -> bool: ... + def exists(self) -> bool: ... + def isdir(self) -> bool: ... + def isfile(self) -> bool: ... + def islink(self) -> bool: ... + def ismount(self) -> bool: ... + def samefile(self, other: str) -> bool: ... + def getatime(self) -> float: ... + @property + def atime(self) -> float: ... + def getmtime(self) -> float: ... + @property + def mtime(self) -> float: ... + def getctime(self) -> float: ... + @property + def ctime(self) -> float: ... + def getsize(self) -> int: ... + @property + def size(self) -> int: ... + def access( + self, + mode: int, + *, + dir_fd: Optional[int] = ..., + effective_ids: bool = ..., + follow_symlinks: bool = ..., + ) -> bool: ... + def stat(self) -> os.stat_result: ... + def lstat(self) -> os.stat_result: ... + def get_owner(self) -> str: ... + @property + def owner(self) -> str: ... + def statvfs(self) -> os.statvfs_result: ... + def pathconf(self, name: Union[str, int]) -> int: ... + def utime( + self, + times: Union[Tuple[int, int], Tuple[float, float], None] = ..., + *, + ns: Tuple[int, int] = ..., + dir_fd: Optional[int] = ..., + follow_symlinks: bool = ..., + ) -> Path: ... + def chmod(self: Self, mode: Union[str, int]) -> Self: ... + def chown(self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ...) -> Self: ... + def rename(self: Self, new: str) -> Self: ... + def renames(self: Self, new: str) -> Self: ... + def mkdir(self: Self, mode: int = ...) -> Self: ... + def mkdir_p(self: Self, mode: int = ...) -> Self: ... + def makedirs(self: Self, mode: int = ...) -> Self: ... + def makedirs_p(self: Self, mode: int = ...) -> Self: ... + def rmdir(self: Self) -> Self: ... + def rmdir_p(self: Self) -> Self: ... + def removedirs(self: Self) -> Self: ... + def removedirs_p(self: Self) -> Self: ... + def touch(self: Self) -> Self: ... + def remove(self: Self) -> Self: ... + def remove_p(self: Self) -> Self: ... + def unlink(self: Self) -> Self: ... + def unlink_p(self: Self) -> Self: ... + def link(self: Self, newpath: str) -> Self: ... + def symlink(self: Self, newlink: Optional[str] = ...) -> Self: ... + def readlink(self: Self) -> Self: ... + def readlinkabs(self: Self) -> Self: ... + def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... + def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... + def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... + def copy(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... + def copy2(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... + def copytree( + self, + dst: str, + symlinks: bool = ..., + ignore: Optional[Callable[[str, list[str]], Iterable[str]]] = ..., + copy_function: Callable[[str, str], None] = ..., + ignore_dangling_symlinks: bool = ..., + dirs_exist_ok: bool = ..., + ) -> Any: ... + def move( + self, dst: str, copy_function: Callable[[str, str], None] = ... + ) -> Any: ... + def rmtree( + self, + ignore_errors: bool = ..., + onerror: Optional[Callable[[Any, Any, Any], Any]] = ..., + ) -> None: ... + def rmtree_p(self: Self) -> Self: ... + def chdir(self) -> None: ... + def cd(self) -> None: ... + def merge_tree( + self, + dst: str, + symlinks: bool = ..., + *, + copy_function: Callable[[str, str], None] = ..., + ignore: Callable[[Any, List[str]], Union[List[str], Set[str]]] = ..., + ) -> None: ... + def chroot(self) -> None: ... + def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... + @contextlib.contextmanager + def in_place( + self, + mode: str = ..., + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + backup_extension: Optional[str] = ..., + ) -> Iterator[Tuple[IO[Any], IO[Any]]]: ... + @classes.ClassProperty + @classmethod + def special(cls) -> Callable[[Optional[str]], SpecialResolver]: ... + +class DirectoryNotEmpty(OSError): + @staticmethod + def translate() -> Iterator[None]: ... + +def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: ... + +class ExtantPath(Path): + def _validate(self) -> None: ... + +class ExtantFile(Path): + def _validate(self) -> None: ... + +class SpecialResolver: + class ResolverScope: + def __init__(self, paths: SpecialResolver, scope: str) -> None: ... + def __getattr__(self, class_: str) -> MultiPathType: ... + def __init__( + self, + path_class: type, + appname: Optional[str] = ..., + appauthor: Optional[str] = ..., + version: Optional[str] = ..., + roaming: bool = ..., + multipath: bool = ..., + ): ... + def __getattr__(self, scope: str) -> ResolverScope: ... + def get_dir(self, scope: str, class_: str) -> MultiPathType: ... + + +class Multi: + @classmethod + def for_class(cls, path_cls: type) -> Type[MultiPathType]: ... + @classmethod + def detect(cls, input: str) -> MultiPathType: ... + def __iter__(self) -> Iterator[Path]: ... + @classes.ClassProperty + @classmethod + def _next_class(cls) -> Type[Path]: ... + + +class MultiPathType(Multi, Path): + pass + +class TempDir(Path): + @classes.ClassProperty + @classmethod + def _next_class(cls) -> Type[Path]: ... + def __new__( + cls: Type[Self], + suffix: Optional[AnyStr] = ..., + prefix: Optional[AnyStr] = ..., + dir: Optional[Union[AnyStr, os.PathLike[AnyStr]]] = ..., + ) -> Self: ... + def __init__(self) -> None: ... + def __enter__(self) -> Path: ... # type: ignore + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: ... + +class Handlers: + @classmethod + def _resolve(cls, param: Union[str, Callable[[str], None]]) -> Callable[[str], None]: ... \ No newline at end of file diff --git a/path/classes.pyi b/path/classes.pyi new file mode 100644 index 00000000..2878c48b --- /dev/null +++ b/path/classes.pyi @@ -0,0 +1,8 @@ +from typing import Any, Callable, Optional + +class ClassProperty(property): + def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: ... + +class multimethod: + def __init__(self, func: Callable[..., Any]): ... + def __get__(self, instance: Any, owner: Optional[type]) -> Any: ... diff --git a/path/masks.pyi b/path/masks.pyi new file mode 100644 index 00000000..d69bf202 --- /dev/null +++ b/path/masks.pyi @@ -0,0 +1,5 @@ +from typing import Any, Callable + +def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: ... +def compound(mode: str) -> Callable[[int], int]: ... +def simple(mode: str) -> Callable[[int], int]: ... diff --git a/path/matchers.pyi b/path/matchers.pyi new file mode 100644 index 00000000..a3b2ff81 --- /dev/null +++ b/path/matchers.pyi @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Any, Callable, Literal, overload + +from path import Path +@overload +def load(param: None) -> Null: ... +@overload +def load(param: str) -> Pattern: ... +@overload +def load(param: Any) -> Any: ... + +class Base: + pass + +class Null(Base): + def __call__(self, path: str) -> Literal[True]: ... + +class Pattern(Base): + def __init__(self, pattern: str) -> None: ... + def get_pattern(self, normcase: Callable[[str], str]) -> str: ... + def __call__(self, path: Path) -> bool: ... + +class CaseInsensitive(Pattern): + normcase: Callable[[str], str] diff --git a/path/py37compat.pyi b/path/py37compat.pyi new file mode 100644 index 00000000..ea62fa06 --- /dev/null +++ b/path/py37compat.pyi @@ -0,0 +1,17 @@ +import os + +from types import ModuleType +from typing import Any, AnyStr, Callable, Dict, Tuple, overload + +def best_realpath(module: ModuleType) -> Callable[[AnyStr], AnyStr]: ... +@overload +def realpath_backport(path: str) -> str: ... +@overload +def realpath_backport(path: bytes) -> bytes: ... +@overload +def _resolve_path(path: str, rest: str, seen: Dict[Any, Any]) -> Tuple[str, bool]: ... +@overload +def _resolve_path( + path: bytes, rest: bytes, seen: Dict[Any, Any] +) -> Tuple[bytes, bool]: ... +def lru_cache(user_function: Callable[..., Any]) -> Callable[..., Any]: ... From 8949d1a1169c9271ceb8aab3f1deea9d82e2fa0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 21:45:52 -0500 Subject: [PATCH 542/835] Add exclusions for pytest 7 deprecations in plugins. Fixes jaraco/skeleton#57. --- pytest.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pytest.ini b/pytest.ini index ec965b24..52f19bea 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,3 +5,14 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 + + # shopkeep/pytest-black#55 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestRemovedIn8Warning + + # tholo/pytest-flake8#83 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestRemovedIn8Warning + + # dbader/pytest-mypy#131 + ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestRemovedIn8Warning From badffe9af9b79dff781f6768bcf48fbd8abd0945 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 14:29:09 -0500 Subject: [PATCH 543/835] Use the parent category PytestDeprecationWarning, which is available on older pytest versions. Fixes jaraco/skeleton#57. --- pytest.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 52f19bea..cbbe3b15 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,11 +8,11 @@ filterwarnings= # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning # tholo/pytest-flake8#83 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning # dbader/pytest-mypy#131 - ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestDeprecationWarning From 96ea56305df99a3c13334d42ea45f779cab2c505 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 20:36:16 -0500 Subject: [PATCH 544/835] Bump pytest-mypy and remove workaround for dbader/pytest-mypy#131. --- pytest.ini | 3 --- setup.cfg | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pytest.ini b/pytest.ini index cbbe3b15..b6880c88 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,6 +13,3 @@ filterwarnings= # tholo/pytest-flake8#83 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning - - # dbader/pytest-mypy#131 - ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestDeprecationWarning diff --git a/setup.cfg b/setup.cfg index bd1da7a2..1b048af5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov - pytest-mypy; \ + pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.0.1 From a9ea801a43fc62a569cf60e1c28e477ba510d8a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 21:58:57 -0500 Subject: [PATCH 545/835] Require jaraco.packaging 9 adding compatibility for projects with no setup.py file. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1b048af5..3b7ac309 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ testing = docs = # upstream sphinx - jaraco.packaging >= 8.2 + jaraco.packaging >= 9 rst.linker >= 1.9 # local From f22eb5b60adbe158e458614ea0380a9071c39347 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 09:50:31 +0000 Subject: [PATCH 546/835] Ignore flake8/black warnings with pytest 7.0.1 (jaraco/skeleton#58) --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index b6880c88..80e98cc9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,7 +9,9 @@ filterwarnings= # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning + ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning # tholo/pytest-flake8#83 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning + ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning From 04fe68a96ee8e3d3ca521b4abbfe53203063f9d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Feb 2022 21:14:39 -0500 Subject: [PATCH 547/835] Ran pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f66bf563..edf6f55f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.1.0 hooks: - id: black From aa8ad8436d3f761f56d8d38077b0cf1c5c1e778b Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Wed, 2 Feb 2022 08:52:10 -0800 Subject: [PATCH 548/835] Add py.typed This file makes it so that other libraries can use path's type hints when running mypy. --- path/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 path/py.typed diff --git a/path/py.typed b/path/py.typed new file mode 100644 index 00000000..e69de29b From efa3f558e02ff5eb5fec804ef718632014b1c45d Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Wed, 2 Feb 2022 10:59:34 -0800 Subject: [PATCH 549/835] Import Literal from typing_extensions This supports mypy on Python 3.7 --- path/__init__.pyi | 20 ++++++++++---------- path/matchers.pyi | 4 +++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index 8657aea9..e2dbfbdf 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -3,14 +3,6 @@ from __future__ import annotations import builtins import contextlib import os -from _typeshed import ( - OpenBinaryMode, - OpenBinaryModeUpdating, - OpenBinaryModeReading, - OpenBinaryModeWriting, - OpenTextMode, - Self, -) from io import ( BufferedRandom, BufferedReader, @@ -29,16 +21,24 @@ from typing import ( Iterator, IO, List, - Literal, Optional, Set, Tuple, Type, - TypeVar, Union, overload, ) +from _typeshed import ( + OpenBinaryMode, + OpenBinaryModeUpdating, + OpenBinaryModeReading, + OpenBinaryModeWriting, + OpenTextMode, + Self, +) +from typing_extensions import Literal + from . import classes # Type for the match argument for several methods diff --git a/path/matchers.pyi b/path/matchers.pyi index a3b2ff81..c534d8ed 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import Any, Callable, Literal, overload +from typing import Any, Callable, overload + +from typing_extensions import Literal from path import Path @overload From 3a00b9a63505aba26301bacd37f321c9b9e662ae Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Wed, 2 Feb 2022 19:46:38 -0800 Subject: [PATCH 550/835] Add platform dependent type hints --- path/__init__.pyi | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index e2dbfbdf..bf60f23f 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -3,6 +3,8 @@ from __future__ import annotations import builtins import contextlib import os +import shutil +import sys from io import ( BufferedRandom, BufferedReader, @@ -42,7 +44,7 @@ from typing_extensions import Literal from . import classes # Type for the match argument for several methods -_Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] +_Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] class TreeWalkWarning(Warning): pass @@ -256,7 +258,7 @@ class Path(str): self, encoding: Optional[str] = ..., errors: Optional[str] = ... ) -> str: ... def read_bytes(self) -> builtins.bytes: ... - def text(self, encoding: Optional[str]=..., errors: str=...) -> str: ... + def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: ... @overload def write_text( self, @@ -324,8 +326,9 @@ class Path(str): def get_owner(self) -> str: ... @property def owner(self) -> str: ... - def statvfs(self) -> os.statvfs_result: ... - def pathconf(self, name: Union[str, int]) -> int: ... + if sys.platform != 'win32': + def statvfs(self) -> os.statvfs_result: ... + def pathconf(self, name: Union[str, int]) -> int: ... def utime( self, times: Union[Tuple[int, int], Tuple[float, float], None] = ..., @@ -335,7 +338,10 @@ class Path(str): follow_symlinks: bool = ..., ) -> Path: ... def chmod(self: Self, mode: Union[str, int]) -> Self: ... - def chown(self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ...) -> Self: ... + if sys.platform != 'win32': + def chown( + self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ... + ) -> Self: ... def rename(self: Self, new: str) -> Self: ... def renames(self: Self, new: str) -> Self: ... def mkdir(self: Self, mode: int = ...) -> Self: ... @@ -388,8 +394,10 @@ class Path(str): copy_function: Callable[[str, str], None] = ..., ignore: Callable[[Any, List[str]], Union[List[str], Set[str]]] = ..., ) -> None: ... - def chroot(self) -> None: ... - def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... + if sys.platform != 'win32': + def chroot(self) -> None: ... + if sys.platform == 'win32': + def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... @contextlib.contextmanager def in_place( self, @@ -432,7 +440,6 @@ class SpecialResolver: def __getattr__(self, scope: str) -> ResolverScope: ... def get_dir(self, scope: str, class_: str) -> MultiPathType: ... - class Multi: @classmethod def for_class(cls, path_cls: type) -> Type[MultiPathType]: ... @@ -443,7 +450,6 @@ class Multi: @classmethod def _next_class(cls) -> Type[Path]: ... - class MultiPathType(Multi, Path): pass @@ -468,4 +474,6 @@ class TempDir(Path): class Handlers: @classmethod - def _resolve(cls, param: Union[str, Callable[[str], None]]) -> Callable[[str], None]: ... \ No newline at end of file + def _resolve( + cls, param: Union[str, Callable[[str], None]] + ) -> Callable[[str], None]: ... From 378326146b547a8696fe09fc30b6c647a8b24bf0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Feb 2022 16:08:19 -0500 Subject: [PATCH 551/835] Update changelog. Ref #207. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b417e9dc..dfbc36e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v16.4.0 +------- + +- #207: Added type hints and declare the library as typed. + v16.3.0 ------- From 1a6b828304e7a8896b55d9ebf83f481ba7ebd568 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 22 Apr 2022 17:43:46 +0200 Subject: [PATCH 552/835] Inject check job into CI workflow as ultimate flag (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a job that is able to accurately signal whether all the expectations of the required jobs to succeed are met. This job can then be used as a source of truth for judging whether "CI passes" and can be used in the branch protection. It also plays a role of a convenient "gate" — this is the only job that would have to be listed in the branch protection as opposed to listing every single job name generated by the test matrix (and they all have different names — it's not possible to just select one `test` job name). Ref: https://github.com/re-actors/alls-green#why --- .github/workflows/main.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5424298d..b54fd6a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,8 +27,23 @@ jobs: - name: Run tests run: tox + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + release: - needs: test + needs: + - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest From 10bf1b1fb9e09e9836bea9e2edec620cd9eea7f9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jun 2022 21:37:40 -0400 Subject: [PATCH 553/835] Add Python 3.11 into the matrix using workaround from actions/setup-python#213. Drop 3.9 from matrix for efficiency. --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b54fd6a1..6468ee0d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,9 +7,11 @@ jobs: strategy: matrix: python: - - 3.7 - - 3.9 - - "3.10" + # Build on pre-releases until stable, then stable releases. + # actions/setup-python#213 + - ~3.7.0-0 + - ~3.10.0-0 + - ~3.11.0-0 platform: - ubuntu-latest - macos-latest From a4f5b769793af19f7b858816889c1bf026f55f5c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 5 Jun 2022 04:47:15 +0300 Subject: [PATCH 554/835] Update base URL for PEPs (#61) --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4ae74093..319b1384 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ ), dict( pattern=r'PEP[- ](?P\d+)', - url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', + url='https://peps.python.org/pep-{pep_number:0>4}/', ), ], ) From 74f337fec4c233b3a6750fa64b61d03c189d9416 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 5 Jun 2022 02:50:24 +0100 Subject: [PATCH 555/835] Update Github actions to v3 (#62) --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6468ee0d..948da052 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,9 +18,9 @@ jobs: - windows-latest runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install tox @@ -50,9 +50,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" - name: Install tox From e719f86c138a750f0c4599cd01cb8067b1ca95c8 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Sun, 5 Jun 2022 15:01:02 -0500 Subject: [PATCH 556/835] exclude build env from cov reporting (#60) * Update .coveragerc * Keep whitespace consistent. Co-authored-by: Jason R. Coombs --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..01164f62 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + */pep517-build-env-* [report] show_missing = True From 6dcd157a7057ec8e1f1f6afebe2115f55df4aaed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 20:57:40 -0400 Subject: [PATCH 557/835] Prefer spaces for rst. Fixes jaraco/skeleton#64. --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index b8aeea17..304196f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 + +[*.rst] +indent_style = space From 2678a7e82d581c07691575d90cd255b64ee63a27 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Jun 2022 15:56:54 -0400 Subject: [PATCH 558/835] Honor PEP 518 with pytest-enabler. --- pyproject.toml | 8 ++++---- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 190b3551..60de2424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,14 @@ skip-string-normalization = true [tool.setuptools_scm] -[pytest.enabler.black] +[tool.pytest-enabler.black] addopts = "--black" -[pytest.enabler.mypy] +[tool.pytest-enabler.mypy] addopts = "--mypy" -[pytest.enabler.flake8] +[tool.pytest-enabler.flake8] addopts = "--flake8" -[pytest.enabler.cov] +[tool.pytest-enabler.cov] addopts = "--cov" diff --git a/setup.cfg b/setup.cfg index 3b7ac309..baa37e5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - pytest-enabler >= 1.0.1 + pytest-enabler >= 1.3 # local From fea1e7cdd57d330f22ac54512ae2df19083c6ec7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jul 2022 18:53:07 -0400 Subject: [PATCH 559/835] Ran pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edf6f55f..af502010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.6.0 hooks: - id: black From 325916c8240b8b3c7c41f24b664ca591e8555ea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 10:12:46 -0400 Subject: [PATCH 560/835] Use '-dev' for every Python version. Ref actions/setup-python#213. --- .github/workflows/main.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 948da052..de49ba8a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,11 +7,9 @@ jobs: strategy: matrix: python: - # Build on pre-releases until stable, then stable releases. - # actions/setup-python#213 - - ~3.7.0-0 - - ~3.10.0-0 - - ~3.11.0-0 + - 3.7 + - '3.10' + - '3.11' platform: - ubuntu-latest - macos-latest @@ -22,7 +20,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python }}-dev - name: Install tox run: | python -m pip install tox From 424717b9e9f7c66379e809eb4e35daae827a1533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 10:18:19 -0400 Subject: [PATCH 561/835] Use Python 3.11 for cutting releases. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de49ba8a..3ce62d92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.11-dev" - name: Install tox run: | python -m pip install tox From c64902b8cafa8062398ef173278a21b042b03a77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jul 2022 19:26:15 -0400 Subject: [PATCH 562/835] Pin flake8. Workaround for tholo/pytest-flake8#87. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index baa37e5e..1ab93501 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,8 @@ testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-flake8 + # workaround for tholo/pytest-flake8#87 + flake8 < 5 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" From abcc15683d3abe229a0e0d07f1afa05a24e2ef8c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Aug 2022 16:06:12 -0400 Subject: [PATCH 563/835] Update to setup-python v4. Fixes jaraco/skeleton#65. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ce62d92..d17b64d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }}-dev - name: Install tox From 47c2cb324e20f784289496ef3a7b19a1cd23d196 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Aug 2022 21:42:40 -0400 Subject: [PATCH 564/835] Also update release to v4 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d17b64d6..63fa1e8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.11-dev" - name: Install tox From 27c55340e745741773e875402d20ecbb7fade521 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:31:09 -0400 Subject: [PATCH 565/835] Add PyPy to the test matrix on Linux. Fixes jaraco/skeleton#63. Adds a 'dev' factor to the matrix as workaround for actions/setup-python#508. --- .github/workflows/main.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 63fa1e8e..46e1ec9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,20 +7,26 @@ jobs: strategy: matrix: python: - - 3.7 - - '3.10' - - '3.11' + - "3.7" + - "3.10" + - "3.11" + # Workaround for actions/setup-python#508 + dev: + - -dev platform: - ubuntu-latest - macos-latest - windows-latest + include: + - python: pypy3.9 + platform: ubuntu-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }}-dev + python-version: ${{ matrix.python }}${{ matrix.dev }} - name: Install tox run: | python -m pip install tox @@ -52,7 +58,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11-dev" + python-version: 3.11-dev - name: Install tox run: | python -m pip install tox From db2c84f7aade320f90cf5bec7afcc3c77d759816 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:39:19 -0400 Subject: [PATCH 566/835] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.pyi | 4 ++++ path/matchers.pyi | 1 + 2 files changed, 5 insertions(+) diff --git a/path/__init__.pyi b/path/__init__.pyi index bf60f23f..a0b8f561 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -329,6 +329,7 @@ class Path(str): if sys.platform != 'win32': def statvfs(self) -> os.statvfs_result: ... def pathconf(self, name: Union[str, int]) -> int: ... + def utime( self, times: Union[Tuple[int, int], Tuple[float, float], None] = ..., @@ -342,6 +343,7 @@ class Path(str): def chown( self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ... ) -> Self: ... + def rename(self: Self, new: str) -> Self: ... def renames(self: Self, new: str) -> Self: ... def mkdir(self: Self, mode: int = ...) -> Self: ... @@ -398,6 +400,7 @@ class Path(str): def chroot(self) -> None: ... if sys.platform == 'win32': def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... + @contextlib.contextmanager def in_place( self, @@ -428,6 +431,7 @@ class SpecialResolver: class ResolverScope: def __init__(self, paths: SpecialResolver, scope: str) -> None: ... def __getattr__(self, class_: str) -> MultiPathType: ... + def __init__( self, path_class: type, diff --git a/path/matchers.pyi b/path/matchers.pyi index c534d8ed..80acd0b1 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -5,6 +5,7 @@ from typing import Any, Callable, overload from typing_extensions import Literal from path import Path + @overload def load(param: None) -> Null: ... @overload From b4f0ae621594316e56cede2856b6a5be605a893c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:46:38 -0400 Subject: [PATCH 567/835] When rendering docs, preserve the syntax for defaults. Fixes jaraco/path#197. Incidentally, re-organize the extensions a bit for clarity. --- docs/conf.py | 12 ++++++++++-- setup.cfg | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 319b1384..9fef70a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] master_doc = "index" +# Link dates and other references in the changelog +extensions += ['rst.linker'] link_files = { '../CHANGES.rst': dict( using=dict(GH='https://github.com'), @@ -25,7 +30,7 @@ ) } -# Be strict about any broken references: +# Be strict about any broken references nitpicky = True # Include Python intersphinx mapping to prevent failures @@ -34,3 +39,6 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True diff --git a/setup.cfg b/setup.cfg index 1ab93501..1d2be997 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ testing = docs = # upstream - sphinx + sphinx >= 3.5 jaraco.packaging >= 9 rst.linker >= 1.9 From 59ff929695e46a59e1fb2e8e6cf8f9d2ace7d434 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:52:39 -0400 Subject: [PATCH 568/835] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index dfbc36e4..f9723b66 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v16.4.1 +------- + +- Refreshed packaging. +- #197: Fixed default argument rendering in docs. + v16.4.0 ------- From c41f2769eb9e60bea5fba000a796f52879663eb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 09:19:27 -0400 Subject: [PATCH 569/835] Remove use of B_NL_END, as bytes can't be used here. --- path/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 5e2193cf..a8bfaed4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -801,8 +801,7 @@ def write_lines( with self.open(mode, encoding=encoding, errors=errors) as f: for line in lines: if linesep is not None: - pattern = U_NL_END if isinstance(line, str) else B_NL_END - line = pattern.sub('', line) + linesep + line = U_NL_END.sub('', line) + linesep f.write(line) def read_md5(self): From 10b8e303a6cd268ae296c7f6e31b73adac60ff4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 09:20:31 -0400 Subject: [PATCH 570/835] Extract _replace_linesep and mark its usage as deprecated. --- path/__init__.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index a8bfaed4..1158dd8e 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -63,6 +63,8 @@ B_NL_END = re.compile(B_NEWLINE.pattern + b'$') U_NL_END = re.compile(U_NEWLINE.pattern + '$') +_default_linesep = object() + class TreeWalkWarning(Warning): pass @@ -759,7 +761,12 @@ def lines(self, encoding=None, errors=None, retain=True): return text.splitlines(retain) def write_lines( - self, lines, encoding=None, errors='strict', linesep=os.linesep, append=False + self, + lines, + encoding=None, + errors='strict', + linesep=_default_linesep, + append=False, ): r"""Write the given lines of text to this file. @@ -776,7 +783,7 @@ def write_lines( `errors` - How to handle errors in Unicode encoding. This also applies only to Unicode strings. - linesep - The desired line-ending. This line-ending is + linesep - (deprecated) The desired line-ending. This line-ending is applied to every line. If a line already has any standard line ending (``'\r'``, ``'\n'``, ``'\r\n'``, ``u'\x85'``, ``u'\r\x85'``, ``u'\u2028'``), that will @@ -799,10 +806,22 @@ def write_lines( """ mode = 'a' if append else 'w' with self.open(mode, encoding=encoding, errors=errors) as f: - for line in lines: - if linesep is not None: - line = U_NL_END.sub('', line) + linesep - f.write(line) + f.writelines(self._replace_linesep(lines, linesep)) + + @staticmethod + def _replace_linesep(lines, linesep): + r""" + >>> U_NL_END.sub('', 'Hello World\r\n') + '\r\n' + 'Hello World\r\n' + """ + if linesep != _default_linesep: + warnings.warn("linesep is deprecated", DeprecationWarning, stacklevel=3) + else: + linesep = os.linesep + if linesep is None: + return lines + + return (U_NL_END.sub('', line) + linesep for line in lines) def read_md5(self): """Calculate the md5 hash for this file. From b8b2f9dcfc3583934a61febb56257e8ae320ed5b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 10:30:28 -0400 Subject: [PATCH 571/835] Override newline setting in open to prevent double-translation. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 1158dd8e..1528bbd6 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -805,7 +805,7 @@ def write_lines( to read the file later. """ mode = 'a' if append else 'w' - with self.open(mode, encoding=encoding, errors=errors) as f: + with self.open(mode, encoding=encoding, errors=errors, newline='') as f: f.writelines(self._replace_linesep(lines, linesep)) @staticmethod From 952d66682df85c70e95e187a638f0798b52667d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 10:33:19 -0400 Subject: [PATCH 572/835] Remove warning, since the input is meant to be lines of text, so shouldn't have any encoding. --- path/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 1528bbd6..8ea8e951 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -795,14 +795,6 @@ def write_lines( Use the keyword argument ``append=True`` to append lines to the file. The default is to overwrite the file. - - .. warning :: - - When you use this with Unicode data, if the encoding of the - existing data in the file is different from the encoding - you specify with the `encoding=` parameter, the result is - mixed-encoding data, which can really confuse someone trying - to read the file later. """ mode = 'a' if append else 'w' with self.open(mode, encoding=encoding, errors=errors, newline='') as f: From aadfa7bb8d82c4a6250967986ed2b0ff08239aef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 11:49:37 -0400 Subject: [PATCH 573/835] Suppress deprecation warnings in tests. --- test_path.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test_path.py b/test_path.py index 89b73b18..102dee93 100644 --- a/test_path.py +++ b/test_path.py @@ -795,14 +795,16 @@ def testLinesep(eol): expected = 2 * cleanNoHanging.replace('\n', eol).encode(encoding) assert p.bytes() == expected - testLinesep('\n') - testLinesep('\r') - testLinesep('\r\n') - testLinesep('\x0d\x85') + with pytest.deprecated_call(): + testLinesep('\n') + testLinesep('\r') + testLinesep('\r\n') + testLinesep('\x0d\x85') # Again, but with linesep=None. - p.write_lines(givenLines, encoding, linesep=None) - p.write_lines(givenLines, encoding, linesep=None, append=True) + with pytest.deprecated_call(): + p.write_lines(givenLines, encoding, linesep=None) + p.write_lines(givenLines, encoding, linesep=None, append=True) # Check the result. expectedBytes = 2 * given.encode(encoding) assert p.bytes() == expectedBytes From 017fb00f0754fc538cc51744221c97c99ecdcf60 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 11:58:56 -0400 Subject: [PATCH 574/835] Extract method for stripping newlines. --- path/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 8ea8e951..f16857f8 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -106,6 +106,14 @@ def __call__(self, walker): traverse = functools.partial(self.follow, item) +def _strip_newlines(lines): + r""" + >>> list(_strip_newlines(['Hello World\r\n', 'foo'])) + ['Hello World', 'foo'] + """ + return (U_NL_END.sub('', line) for line in lines) + + class Path(str): """ Represents a filesystem path. @@ -802,10 +810,6 @@ def write_lines( @staticmethod def _replace_linesep(lines, linesep): - r""" - >>> U_NL_END.sub('', 'Hello World\r\n') + '\r\n' - 'Hello World\r\n' - """ if linesep != _default_linesep: warnings.warn("linesep is deprecated", DeprecationWarning, stacklevel=3) else: @@ -813,7 +817,7 @@ def _replace_linesep(lines, linesep): if linesep is None: return lines - return (U_NL_END.sub('', line) + linesep for line in lines) + return (line + linesep for line in _strip_newlines(lines)) def read_md5(self): """Calculate the md5 hash for this file. From 50938d1c581acc32adde9fa5a09b014d6c809957 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 12:09:52 -0400 Subject: [PATCH 575/835] Add masks module to docs --- docs/api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index cc77f4fa..150d9358 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,3 +9,7 @@ API .. automodule:: path :members: :undoc-members: + +.. automodule:: path.masks + :members: + :undoc-members: From a00322d5cbe1f82089483ec2b806f28f94716b4c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 12:15:08 -0400 Subject: [PATCH 576/835] Update changelog. --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f9723b66..ad0fb0e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,10 @@ -v16.4.1 +v16.5.0 ------- - Refreshed packaging. - #197: Fixed default argument rendering in docs. +- #209: Refactored ``write_lines`` to re-use open semantics. + Deprecated the ``linesep`` parameter. v16.4.0 ------- From 679eebb215c80c7376a1df02c77fd368347620b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 12:41:25 -0400 Subject: [PATCH 577/835] Adopt furo theme for docs. --- docs/conf.py | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 9fef70a5..fa741a85 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,6 +7,7 @@ ] master_doc = "index" +html_theme = "furo" # Link dates and other references in the changelog extensions += ['rst.linker'] diff --git a/setup.cfg b/setup.cfg index 1d2be997..a0d86eba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ docs = sphinx >= 3.5 jaraco.packaging >= 9 rst.linker >= 1.9 + furo # local From b2412262dc1dd5d3d697e551d86acee4d5519bb6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 13:30:09 -0400 Subject: [PATCH 578/835] Indicate to use latest Python version (workaround for readthedocs/readthedocs.org/#9623). Requires also specifying the OS version (workaround for readthedocs/readthedocs.org#9635). --- .readthedocs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index cc698548..6bef3493 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,3 +4,10 @@ python: - path: . extra_requirements: - docs + +# workaround for readthedocs/readthedocs.org#9623 +build: + # workaround for readthedocs/readthedocs.org#9635 + os: ubuntu-22.04 + tools: + python: "3" From e95c54fe607aaa980a064b6490312483381ba0ab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Oct 2022 11:35:13 -0400 Subject: [PATCH 579/835] GHA pretty env (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 Make the GHA log is clean and colorized This patch sets up root-level environment variables shared by all the workflow jobs. They include: * Disabling undesired `pip`'s warnings/suggestions * Requesting the executed apps color their output unconditionally * Letting `tox` pass those requests to underlying/wrapped programs * Reformat without end of line comments. Group into sections. * Avoid numerics for booleans where possible. Choose arbitrary numeric where any numeric is accepted. Co-authored-by: Sviatoslav Sydorenko --- .github/workflows/main.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 46e1ec9c..102e0e2b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,36 @@ name: tests on: [push, pull_request] +env: + # Environment variables to support color support (jaraco/skeleton#66): + # Request colored output from CLI tools supporting it. Different tools + # interpret the value differently. For some, just being set is sufficient. + # For others, it must be a non-zero integer. For yet others, being set + # to a non-empty value is sufficient. + FORCE_COLOR: -106 + # MyPy's color enforcement (must be a non-zero number) + MYPY_FORCE_COLOR: -42 + # Recognized by the `py` package, dependency of `pytest` (must be "1") + PY_COLORS: 1 + # Make tox-wrapped tools see color requests + TOX_TESTENV_PASSENV: >- + FORCE_COLOR + MYPY_FORCE_COLOR + NO_COLOR + PY_COLORS + PYTEST_THEME + PYTEST_THEME_MODE + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_PYTHON_VERSION_WARNING: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream + # Must be "1". + TOX_PARALLEL_NO_SPINNER: 1 + + jobs: test: strategy: From 54675240d4b4d2452a3777c5156f688e42a6c985 Mon Sep 17 00:00:00 2001 From: Zach Burnett Date: Thu, 13 Oct 2022 15:00:05 -0400 Subject: [PATCH 580/835] rename `.readthedocs.yml` to `.readthedocs.yaml` (RTD docs indicate that `.readthedocs.yml` will be deprecated) (#68) --- .readthedocs.yml => .readthedocs.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .readthedocs.yml => .readthedocs.yaml (100%) diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 100% rename from .readthedocs.yml rename to .readthedocs.yaml From da84e5c7dabacf379165a0829b2f1741060ee2c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Nov 2022 05:25:30 -0500 Subject: [PATCH 581/835] Pin mypy to '<0.990' due to realpython/pytest-mypy#141 --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index a0d86eba..503cbfda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,8 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" + # workaround for realpython/pytest-mypy#141 + mypy < 0.990 pytest-enabler >= 1.3 # local From f999a531587170b577da64d4bfb67a68b9aec106 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Oct 2022 11:14:42 -0400 Subject: [PATCH 582/835] Remove the hyperlink for the Python versions badge. The PyPI badge is a better anchor for the hyperlink. --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c82c6429..39459a4a 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,7 @@ .. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: `PyPI link`_ + :target: https://pypi.org/project/skeleton .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg - :target: `PyPI link`_ - -.. _PyPI link: https://pypi.org/project/skeleton .. image:: https://github.com/jaraco/skeleton/workflows/tests/badge.svg :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22tests%22 From 401287d8d0f9fb0365149983f5ca42618f00a6d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 9 Nov 2022 19:32:49 -0500 Subject: [PATCH 583/835] Apply explicit_package_bases for mypy and unpin the version. Ref python/mypy#14057. --- mypy.ini | 3 +++ setup.cfg | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 976ba029..b6f97276 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,5 @@ [mypy] ignore_missing_imports = True +# required to support namespace packages +# https://github.com/python/mypy/issues/14057 +explicit_package_bases = True diff --git a/setup.cfg b/setup.cfg index 503cbfda..a0d86eba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,8 +40,6 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - # workaround for realpython/pytest-mypy#141 - mypy < 0.990 pytest-enabler >= 1.3 # local From 56b6f1d1d7a975b27f96c4e15a20077914b4c554 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 18 Nov 2022 22:32:51 -0500 Subject: [PATCH 584/835] Add Python 3.12 to matrix. Only test 3.8-3.10 on Linux. --- .github/workflows/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 102e0e2b..3a28be36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,8 +38,8 @@ jobs: matrix: python: - "3.7" - - "3.10" - "3.11" + - "3.12" # Workaround for actions/setup-python#508 dev: - -dev @@ -48,6 +48,12 @@ jobs: - macos-latest - windows-latest include: + - python: "3.8" + platform: ubuntu-latest + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} From 9e13598ce4b81c2c964dd555fa407bb3ba4cc607 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2022 09:36:01 -0500 Subject: [PATCH 585/835] Disable flake8 on Python 3.12. Workaround for tholo/pytest-flake8#87. --- setup.cfg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a0d86eba..a8f80ced 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,9 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-flake8 + pytest-flake8; \ + # workaround for tholo/pytest-flake8#87 + python_version < "3.12" # workaround for tholo/pytest-flake8#87 flake8 < 5 pytest-black >= 0.3.7; \ From f1c302a3decad954f000a63a30efe67976c91835 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Nov 2022 15:58:25 -0500 Subject: [PATCH 586/835] Suppress install of pywin32 on Python 3.12 until it's available. --- setup.cfg | 2 +- test_path.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index eeffa36d..0d4c33de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,7 @@ testing = # local appdirs packaging - pywin32; platform_system == "Windows" + pywin32; platform_system == "Windows" and python_version < "3.12" # required for checkdocs on README.rst pygments diff --git a/test_path.py b/test_path.py index 102dee93..dc7b651d 100644 --- a/test_path.py +++ b/test_path.py @@ -334,6 +334,7 @@ def test_import_time(self, monkeypatch): class TestOwnership: + @pytest.mark.skipif('platform.system() == "Windows" and sys.version_info > (3, 12)') def test_get_owner(self): Path('/').get_owner() From 6debbcecd357c30b080ea19bf2cfa0e2ab0fcd50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Nov 2022 15:45:57 -0500 Subject: [PATCH 587/835] Add setter support for .mtime and .atime. --- CHANGES.rst | 5 +++++ path/__init__.py | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ad0fb0e4..63ecb163 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v16.6.0 +------- + +- ``.mtime`` and ``.atime`` are now settable. + v16.5.0 ------- diff --git a/path/__init__.py b/path/__init__.py index f16857f8..d61bb3c8 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -37,6 +37,9 @@ import io import importlib import itertools +import datetime +from numbers import Number +from typing import Union with contextlib.suppress(ImportError): import win32security @@ -66,6 +69,11 @@ _default_linesep = object() +def _make_timestamp_ns(value: Union[Number, datetime.datetime]) -> Number: + timestamp_s = value if isinstance(value, Number) else value.timestamp() + return int(timestamp_s * 10**9) + + class TreeWalkWarning(Warning): pass @@ -906,9 +914,13 @@ def getatime(self): """.. seealso:: :attr:`atime`, :func:`os.path.getatime`""" return self.module.getatime(self) + def set_atime(self, value): + mtime_ns = self.stat().st_atime_ns + self.utime(ns=(_make_timestamp_ns(value), mtime_ns)) + atime = property( getatime, - None, + set_atime, None, """ Last access time of the file. @@ -916,6 +928,14 @@ def getatime(self): >>> Path('.').atime > 0 True + Allows setting: + + >>> some_file = Path(getfixture('tmp_path')).joinpath('file.txt').touch() + >>> MST = datetime.timezone(datetime.timedelta(hours=-7)) + >>> some_file.atime = datetime.datetime(1976, 5, 7, 10, tzinfo=MST) + >>> some_file.atime + 200336400.0 + .. seealso:: :meth:`getatime`, :func:`os.path.getatime` """, ) @@ -924,13 +944,25 @@ def getmtime(self): """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`""" return self.module.getmtime(self) + def set_mtime(self, value): + atime_ns = self.stat().st_atime_ns + self.utime(ns=(atime_ns, _make_timestamp_ns(value))) + mtime = property( getmtime, - None, + set_mtime, None, """ Last modified time of the file. + Allows setting: + + >>> some_file = Path(getfixture('tmp_path')).joinpath('file.txt').touch() + >>> MST = datetime.timezone(datetime.timedelta(hours=-7)) + >>> some_file.mtime = datetime.datetime(1976, 5, 7, 10, tzinfo=MST) + >>> some_file.mtime + 200336400.0 + .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` """, ) From 9708c37ef0d286c4e907adc59f46cc92262e3bf1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Dec 2022 09:10:17 -0500 Subject: [PATCH 588/835] Honor ResourceWarnings. Fixes jaraco/skeleton#73. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 80e98cc9..2c2817b8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,9 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= + # Ensure ResourceWarnings are emitted + default::ResourceWarning + # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 From 86a55c8320e2706d0f92e3248c29351bff83da4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Dec 2022 19:18:02 -0500 Subject: [PATCH 589/835] tox 4 requires a boolean value, so use '1' to FORCE_COLOR. Fixes jaraco/skeleton#74. --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a28be36..e1e7bf19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,8 +7,10 @@ env: # Request colored output from CLI tools supporting it. Different tools # interpret the value differently. For some, just being set is sufficient. # For others, it must be a non-zero integer. For yet others, being set - # to a non-empty value is sufficient. - FORCE_COLOR: -106 + # to a non-empty value is sufficient. For tox, it must be one of + # , 0, 1, false, no, off, on, true, yes. The only enabling value + # in common is "1". + FORCE_COLOR: 1 # MyPy's color enforcement (must be a non-zero number) MYPY_FORCE_COLOR: -42 # Recognized by the `py` package, dependency of `pytest` (must be "1") From ef521390cb51a12eab5c4155900f45dc2c89d507 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 Dec 2022 23:17:14 -0500 Subject: [PATCH 590/835] Remove unnecessary shebang and encoding header in docs conf. --- docs/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fa741a85..c2043393 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From c68ac3b7a3001502f681722dc55dff70a3169276 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Dec 2022 21:04:34 -0500 Subject: [PATCH 591/835] Prevent Python 3.12 from blocking checks. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1e7bf19..9d02856b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,6 +59,7 @@ jobs: - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.python == '3.12' }} steps: - uses: actions/checkout@v3 - name: Setup Python From 82465b907d5131a57862a7242d64d610c3a05039 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Dec 2022 20:46:15 -0500 Subject: [PATCH 592/835] Build docs in CI, including sphinx-lint. --- .github/workflows/main.yml | 17 +++++++++++++++++ setup.cfg | 1 + tox.ini | 1 + 3 files changed, 19 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d02856b..9629a26a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -72,11 +72,28 @@ jobs: - name: Run tests run: tox + docs: + runs-on: ubuntu-latest + env: + TOXENV: docs + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }}${{ matrix.dev }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + check: # This job does nothing and is only used for the branch protection if: always() needs: - test + - docs runs-on: ubuntu-latest diff --git a/setup.cfg b/setup.cfg index a8f80ced..c062c7b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ docs = jaraco.packaging >= 9 rst.linker >= 1.9 furo + sphinx-lint # local diff --git a/tox.ini b/tox.ini index 3ca2af38..42ae6852 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint [testenv:release] skip_install = True From a6c6660d71fcd9f55d4ddbb4cd411ab34cc38ec9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2022 19:55:26 -0500 Subject: [PATCH 593/835] Put tidelift docs dependency in its own section to limit merge conflicts. --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 798b1033..cdb0caa9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [options.extras_require] docs = - # upstream + + # tidelift jaraco.tidelift >= 1.4 From eb2bdc83a7d3cfd1c2bc3aeae39a900d654a6839 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2023 03:17:24 -0500 Subject: [PATCH 594/835] Update badge for 2023 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 39459a4a..af0efb05 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2022-informational +.. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton From f9e01d2197d18b2b21976bae6e5b7f90b683bc4f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 18 Jan 2023 21:33:18 -0500 Subject: [PATCH 595/835] ALLOW_UNICODE no longer needed on Python 3. As a result, ELLIPSES is also now enabled by default. --- pytest.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 2c2817b8..1e6adf08 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules -doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning From 284359e5123eb6a9f975092d1fb17dfa814d1594 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 17:56:30 -0500 Subject: [PATCH 596/835] Enable default encoding warning where available. See PEP 597. --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42ae6852..5a678211 100644 --- a/tox.ini +++ b/tox.ini @@ -8,10 +8,13 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = +setenv = + PYTHONWARNDEFAULTENCODING = 1 commands = pytest {posargs} usedevelop = True -extras = testing +extras = + testing [testenv:docs] extras = From f18255faba76a6a86bf3fa6f73da9d974262aebd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 18:19:23 -0500 Subject: [PATCH 597/835] Suppress EncodingWarning in pytest_black. Workaround for shopkeep/pytest-black#67. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 1e6adf08..bd7d0b52 100644 --- a/pytest.ini +++ b/pytest.ini @@ -17,3 +17,6 @@ filterwarnings= ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning + + # shopkeep/pytest-black#67 + ignore:'encoding' argument not specified::pytest_black From 0d9c6f0f5b6182cdac448270dbc0529f91b50bd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 18:50:22 -0500 Subject: [PATCH 598/835] Exempt warning. Workaround for realpython/pytest-mypy#152 --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index bd7d0b52..69d95b26 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,3 +20,6 @@ filterwarnings= # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy From 5f095d18d76f7ae36e57fa3241da341b0f9cd365 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Feb 2023 09:54:15 -0500 Subject: [PATCH 599/835] Add #upstream markers for filtered warnings. Add filter for platform module (ref python/cpython#100750). --- pytest.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytest.ini b/pytest.ini index 69d95b26..5b6ddc45 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,8 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules filterwarnings= + ## upstream + # Ensure ResourceWarnings are emitted default::ResourceWarning @@ -23,3 +25,8 @@ filterwarnings= # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore::EncodingWarning:platform + + ## end upstream From 6f7ac885c61eb74df8c2db435cdbec412da06fe6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 9 Feb 2023 03:52:03 -0500 Subject: [PATCH 600/835] Remove reference to EncodingWarning as it doesn't exist on some Pythons. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 5b6ddc45..99a25199 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,6 @@ filterwarnings= ignore:'encoding' argument not specified::pytest_mypy # python/cpython#100750 - ignore::EncodingWarning:platform + ignore:'encoding' argument not specified::platform ## end upstream From 9650fc184fc120a21623d8f92d03ee4ccbaa89d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 16:43:22 -0500 Subject: [PATCH 601/835] Revert "exclude build env from cov reporting (jaraco/skeleton#60)" This reverts commit e719f86c138a750f0c4599cd01cb8067b1ca95c8. The issue seems to have been addressed somehow. Ref pytest-dev/pytest-cov#538. --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 01164f62..6a34e662 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,6 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* - */pep517-build-env-* [report] show_missing = True From 56cdf46aa19450d58b4a56af6553a0225762ae4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 21:12:36 -0500 Subject: [PATCH 602/835] Disable couldnt-parse warnings. Prescribed workaround for nedbat/coveragepy#1392. Fixes python/importlib_resources#279 and fixes jaraco/skeleton#56. --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..02879483 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,8 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* +disable_warnings = + couldnt-parse [report] show_missing = True From 109f8c09ddb4904dc3f83307473520b2250ccb30 Mon Sep 17 00:00:00 2001 From: Joyce Date: Sat, 18 Mar 2023 13:25:16 -0300 Subject: [PATCH 603/835] Feat: initial permissions to main.yml (jaraco/skeleton#76) Signed-off-by: Joyce --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9629a26a..3fa1c81e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,9 @@ name: tests on: [push, pull_request] +permissions: + contents: read + env: # Environment variables to support color support (jaraco/skeleton#66): # Request colored output from CLI tools supporting it. Different tools @@ -104,6 +107,8 @@ jobs: jobs: ${{ toJSON(needs) }} release: + permissions: + contents: write needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') From 5957d58266e479f124b31f30e4322e798fdf386b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Apr 2023 21:57:33 -0400 Subject: [PATCH 604/835] Remove unnecessary and incorrect copyright notice. Fixes jaraco/skeleton#78. --- LICENSE | 2 -- 1 file changed, 2 deletions(-) diff --git a/LICENSE b/LICENSE index 353924be..1bb5a443 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the From d2ec0473f8d4c25cc6f696e70ba110e1061e4dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 May 2023 20:27:17 -0400 Subject: [PATCH 605/835] Replace flake8 with ruff. Fixes jaraco/skeleton#79 and sheds debt. --- .flake8 | 9 --------- pyproject.toml | 6 +++--- pytest.ini | 8 -------- setup.cfg | 6 +----- 4 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 48b2e246..00000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 88 - -# jaraco/skeleton#34 -max-complexity = 10 - -extend-ignore = - # Black creates whitespace before colon - E203 diff --git a/pyproject.toml b/pyproject.toml index 60de2424..d5f3487e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ addopts = "--black" [tool.pytest-enabler.mypy] addopts = "--mypy" -[tool.pytest-enabler.flake8] -addopts = "--flake8" - [tool.pytest-enabler.cov] addopts = "--cov" + +[tool.pytest-enabler.ruff] +addopts = "--ruff" diff --git a/pytest.ini b/pytest.ini index 99a25199..94515aaf 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,19 +7,11 @@ filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning - # Suppress deprecation warning in flake8 - ignore:SelectableGroups dict interface is deprecated::flake8 - # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - # tholo/pytest-flake8#83 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning - ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning - # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black diff --git a/setup.cfg b/setup.cfg index c062c7b9..6b31311e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,11 +30,6 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-flake8; \ - # workaround for tholo/pytest-flake8#87 - python_version < "3.12" - # workaround for tholo/pytest-flake8#87 - flake8 < 5 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" @@ -43,6 +38,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.3 + pytest-ruff # local From 96ebfe14538c2279b54dd19567e5922880b4fdf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 12:31:51 -0400 Subject: [PATCH 606/835] Make substitution fields more prominent and distinct from true 'skeleton' references. (#71) Fixes #70 --- README.rst | 14 +++++++------- docs/index.rst | 2 +- setup.cfg | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index af0efb05..1f66d195 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,18 @@ -.. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: https://pypi.org/project/skeleton +.. image:: https://img.shields.io/pypi/v/PROJECT.svg + :target: https://pypi.org/project/PROJECT -.. image:: https://img.shields.io/pypi/pyversions/skeleton.svg +.. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg -.. image:: https://github.com/jaraco/skeleton/workflows/tests/badge.svg - :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22tests%22 +.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg + :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black -.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest -.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest +.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest +.. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton diff --git a/docs/index.rst b/docs/index.rst index 325842bb..53117d16 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Welcome to |project| documentation! history -.. automodule:: skeleton +.. automodule:: PROJECT :members: :undoc-members: :show-inheritance: diff --git a/setup.cfg b/setup.cfg index 6b31311e..0cee3d34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,10 @@ [metadata] -name = skeleton +name = PROJECT author = Jason R. Coombs author_email = jaraco@jaraco.com -description = skeleton +description = PROJECT_DESCRIPTION long_description = file:README.rst -url = https://github.com/jaraco/skeleton +url = https://github.com/PROJECT_PATH classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers From 4ce054b47df31b4845968043c8772ee4a604390a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 20:15:16 -0400 Subject: [PATCH 607/835] Suppress EncodingWarning in build.env. Ref pypa/build#615. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 94515aaf..3d30458f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -21,4 +21,7 @@ filterwarnings= # python/cpython#100750 ignore:'encoding' argument not specified::platform + # pypa/build#615 + ignore:'encoding' argument not specified:EncodingWarning:build.env + ## end upstream From a0acaace3e29937d0711b3de8019cd3fe4799cf7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 20:29:31 -0400 Subject: [PATCH 608/835] Remove reference to EncodingWarning as it doesn't exist on some Pythons. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 3d30458f..d9a15ed1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -22,6 +22,6 @@ filterwarnings= ignore:'encoding' argument not specified::platform # pypa/build#615 - ignore:'encoding' argument not specified:EncodingWarning:build.env + ignore:'encoding' argument not specified::build.env ## end upstream From 6f754807a0abd25e0b52f024df2072d53f336974 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 7 Jun 2023 09:49:46 -0400 Subject: [PATCH 609/835] Update RTD boilerplate to new issue. Ref readthedocs/readthedocs.org#10401. --- .readthedocs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 6bef3493..053c7287 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,9 +5,8 @@ python: extra_requirements: - docs -# workaround for readthedocs/readthedocs.org#9623 +# required boilerplate readthedocs/readthedocs.org#10401 build: - # workaround for readthedocs/readthedocs.org#9635 os: ubuntu-22.04 tools: python: "3" From e7cd730d0d708c8f1f3eb28a29927f3475b3e855 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 21:27:17 -0400 Subject: [PATCH 610/835] Add badge for Ruff. --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 1f66d195..b703d490 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,10 @@ :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black From 241541c07c9c30e48b57d59e527ef923d05c82d6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jun 2023 14:11:22 -0400 Subject: [PATCH 611/835] Remove inclusion of python version for docs --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3fa1c81e..93471ce8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -83,8 +83,6 @@ jobs: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python }}${{ matrix.dev }} - name: Install tox run: | python -m pip install tox From 74b0d396c87892e9122c96994cf2c26329141208 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Jun 2023 21:11:01 -0400 Subject: [PATCH 612/835] Adopt towncrier for managing changelog. Fixes jaraco/skeleton#83. Renamed CHANGES.rst to NEWS.rst to align with towncrier defaults. --- CHANGES.rst => NEWS.rst | 0 docs/conf.py | 2 +- docs/history.rst | 2 +- towncrier.toml | 2 ++ tox.ini | 9 +++++++++ 5 files changed, 13 insertions(+), 2 deletions(-) rename CHANGES.rst => NEWS.rst (100%) create mode 100644 towncrier.toml diff --git a/CHANGES.rst b/NEWS.rst similarity index 100% rename from CHANGES.rst rename to NEWS.rst diff --git a/docs/conf.py b/docs/conf.py index c2043393..32150488 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ # Link dates and other references in the changelog extensions += ['rst.linker'] link_files = { - '../CHANGES.rst': dict( + '../NEWS.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( diff --git a/docs/history.rst b/docs/history.rst index 8e217503..5bdc2320 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -5,4 +5,4 @@ History ******* -.. include:: ../CHANGES (links).rst +.. include:: ../NEWS (links).rst diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini index 5a678211..32b031da 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,15 @@ commands = python -m sphinx -W --keep-going . {toxinidir}/build/html python -m sphinxlint +[testenv:finalize] +skip_install = True +deps = + towncrier + jaraco.develop +passenv = * +commands = + python -m jaraco.develop.towncrier build --yes + [testenv:release] skip_install = True deps = From cd145f4080ef0e954aa4716fc3f240c508a5693c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Jun 2023 23:34:21 -0400 Subject: [PATCH 613/835] Replace workaround for actions/setup-python#508 with 'allow-prereleases' --- .github/workflows/main.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93471ce8..00b21297 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,9 +45,6 @@ jobs: - "3.7" - "3.11" - "3.12" - # Workaround for actions/setup-python#508 - dev: - - -dev platform: - ubuntu-latest - macos-latest @@ -68,7 +65,8 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }}${{ matrix.dev }} + python-version: ${{ matrix.python }} + allow-prereleases: true - name: Install tox run: | python -m pip install tox From 07a87ea9d8671ea4f529858201866e3f78fa3afc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Jun 2023 12:59:50 -0400 Subject: [PATCH 614/835] Remove tox boilerplate, no longer necessary with later versions of tox. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 32b031da..4e8e7090 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,4 @@ [tox] -envlist = python -minversion = 3.2 -# https://github.com/jaraco/skeleton/issues/6 -tox_pip_extensions_ext_venv_update = true toxworkdir={env:TOX_WORK_DIR:.tox} From 3b7d8a912d54ccf88f79eea0dfc903d101067bb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 20:55:42 -0400 Subject: [PATCH 615/835] Require Python 3.8 or later. --- .github/workflows/main.yml | 4 +--- newsfragments/+drop-py37.feature.rst | 1 + setup.cfg | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00b21297..7cc4fb82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,7 @@ jobs: strategy: matrix: python: - - "3.7" + - "3.8" - "3.11" - "3.12" platform: @@ -50,8 +50,6 @@ jobs: - macos-latest - windows-latest include: - - python: "3.8" - platform: ubuntu-latest - python: "3.9" platform: ubuntu-latest - python: "3.10" diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst new file mode 100644 index 00000000..ccabdaa3 --- /dev/null +++ b/newsfragments/+drop-py37.feature.rst @@ -0,0 +1 @@ +Require Python 3.8 or later. diff --git a/setup.cfg b/setup.cfg index 0cee3d34..75a50d4d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = [options] packages = find_namespace: include_package_data = true -python_requires = >=3.7 +python_requires = >=3.8 install_requires = [options.packages.find] From 8e83c3f0bc7baab5f2db37487526e374a1f68494 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Jun 2023 19:52:23 -0400 Subject: [PATCH 616/835] Expand 'finalize' to commit and tag the change. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 4e8e7090..1093e028 100644 --- a/tox.ini +++ b/tox.ini @@ -25,10 +25,11 @@ commands = skip_install = True deps = towncrier - jaraco.develop + jaraco.develop >= 7.23 passenv = * commands = - python -m jaraco.develop.towncrier build --yes + python -m jaraco.develop.finalize + [testenv:release] skip_install = True From 74ba8acbe019de9f30dee6d319c8621caac070ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jun 2023 22:23:40 -0400 Subject: [PATCH 617/835] Leverage pytest-enabler 2.2 for the default config. --- pyproject.toml | 12 ------------ setup.cfg | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5f3487e..dce944df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,3 @@ build-backend = "setuptools.build_meta" skip-string-normalization = true [tool.setuptools_scm] - -[tool.pytest-enabler.black] -addopts = "--black" - -[tool.pytest-enabler.mypy] -addopts = "--mypy" - -[tool.pytest-enabler.cov] -addopts = "--cov" - -[tool.pytest-enabler.ruff] -addopts = "--ruff" diff --git a/setup.cfg b/setup.cfg index 75a50d4d..a9ca2a88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - pytest-enabler >= 1.3 + pytest-enabler >= 2.2 pytest-ruff # local From cca49a4481167049f6bdd0f8038e685e5b8e929f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jul 2023 15:18:58 -0400 Subject: [PATCH 618/835] Prefer 3.x for Python version (latest stable). --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cc4fb82..f54dfbc6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -113,7 +113,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: 3.11-dev + python-version: 3.x - name: Install tox run: | python -m pip install tox From c29955f9be8e44b2ea5fea12f86b7bd46a0b3958 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Jul 2023 11:53:15 -0400 Subject: [PATCH 619/835] Collapse skeleton history. Workaround for jaraco/skeleton#87. --- .coveragerc | 9 ++ .editorconfig | 19 ++++ .github/dependabot.yml | 8 ++ .github/workflows/main.yml | 124 +++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 ++ .readthedocs.yaml | 12 +++ LICENSE | 17 ++++ NEWS.rst | 0 README.rst | 22 +++++ docs/conf.py | 42 +++++++++ docs/history.rst | 8 ++ docs/index.rst | 22 +++++ mypy.ini | 5 ++ newsfragments/+drop-py37.feature.rst | 1 + pyproject.toml | 8 ++ pytest.ini | 27 ++++++ setup.cfg | 55 ++++++++++++ towncrier.toml | 2 + tox.ini | 49 +++++++++++ 19 files changed, 435 insertions(+) create mode 100644 .coveragerc create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 LICENSE create mode 100644 NEWS.rst create mode 100644 README.rst create mode 100644 docs/conf.py create mode 100644 docs/history.rst create mode 100644 docs/index.rst create mode 100644 mypy.ini create mode 100644 newsfragments/+drop-py37.feature.rst create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 towncrier.toml create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..02879483 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* +disable_warnings = + couldnt-parse + +[report] +show_missing = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..304196f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space +max_line_length = 88 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rst] +indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..f54dfbc6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,124 @@ +name: tests + +on: [push, pull_request] + +permissions: + contents: read + +env: + # Environment variables to support color support (jaraco/skeleton#66): + # Request colored output from CLI tools supporting it. Different tools + # interpret the value differently. For some, just being set is sufficient. + # For others, it must be a non-zero integer. For yet others, being set + # to a non-empty value is sufficient. For tox, it must be one of + # , 0, 1, false, no, off, on, true, yes. The only enabling value + # in common is "1". + FORCE_COLOR: 1 + # MyPy's color enforcement (must be a non-zero number) + MYPY_FORCE_COLOR: -42 + # Recognized by the `py` package, dependency of `pytest` (must be "1") + PY_COLORS: 1 + # Make tox-wrapped tools see color requests + TOX_TESTENV_PASSENV: >- + FORCE_COLOR + MYPY_FORCE_COLOR + NO_COLOR + PY_COLORS + PYTEST_THEME + PYTEST_THEME_MODE + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_PYTHON_VERSION_WARNING: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream + # Must be "1". + TOX_PARALLEL_NO_SPINNER: 1 + + +jobs: + test: + strategy: + matrix: + python: + - "3.8" + - "3.11" + - "3.12" + platform: + - ubuntu-latest + - macos-latest + - windows-latest + include: + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest + - python: pypy3.9 + platform: ubuntu-latest + runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.python == '3.12' }} + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + docs: + runs-on: ubuntu-latest + env: + TOXENV: docs + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + - docs + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + release: + permissions: + contents: write + needs: + - check + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..af502010 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..053c7287 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs + +# required boilerplate readthedocs/readthedocs.org#10401 +build: + os: ubuntu-22.04 + tools: + python: "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1bb5a443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/NEWS.rst b/NEWS.rst new file mode 100644 index 00000000..e69de29b diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..b703d490 --- /dev/null +++ b/README.rst @@ -0,0 +1,22 @@ +.. image:: https://img.shields.io/pypi/v/PROJECT.svg + :target: https://pypi.org/project/PROJECT + +.. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg + +.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg + :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest +.. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2023-informational + :target: https://blog.jaraco.com/skeleton diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..32150488 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,42 @@ +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] + +master_doc = "index" +html_theme = "furo" + +# Link dates and other references in the changelog +extensions += ['rst.linker'] +link_files = { + '../NEWS.rst': dict( + using=dict(GH='https://github.com'), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://peps.python.org/pep-{pep_number:0>4}/', + ), + ], + ) +} + +# Be strict about any broken references +nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..5bdc2320 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../NEWS (links).rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..53117d16 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to |project| documentation! +=================================== + +.. toctree:: + :maxdepth: 1 + + history + + +.. automodule:: PROJECT + :members: + :undoc-members: + :show-inheritance: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b6f97276 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True +# required to support namespace packages +# https://github.com/python/mypy/issues/14057 +explicit_package_bases = True diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst new file mode 100644 index 00000000..ccabdaa3 --- /dev/null +++ b/newsfragments/+drop-py37.feature.rst @@ -0,0 +1 @@ +Require Python 3.8 or later. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dce944df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" + +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..d9a15ed1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,27 @@ +[pytest] +norecursedirs=dist build .tox .eggs +addopts=--doctest-modules +filterwarnings= + ## upstream + + # Ensure ResourceWarnings are emitted + default::ResourceWarning + + # shopkeep/pytest-black#55 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning + ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning + + # shopkeep/pytest-black#67 + ignore:'encoding' argument not specified::pytest_black + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore:'encoding' argument not specified::platform + + # pypa/build#615 + ignore:'encoding' argument not specified::build.env + + ## end upstream diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..a9ca2a88 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,55 @@ +[metadata] +name = PROJECT +author = Jason R. Coombs +author_email = jaraco@jaraco.com +description = PROJECT_DESCRIPTION +long_description = file:README.rst +url = https://github.com/PROJECT_PATH +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + +[options] +packages = find_namespace: +include_package_data = true +python_requires = >=3.8 +install_requires = + +[options.packages.find] +exclude = + build* + dist* + docs* + tests* + +[options.extras_require] +testing = + # upstream + pytest >= 6 + pytest-checkdocs >= 2.4 + pytest-black >= 0.3.7; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-cov + pytest-mypy >= 0.9.1; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-enabler >= 2.2 + pytest-ruff + + # local + +docs = + # upstream + sphinx >= 3.5 + jaraco.packaging >= 9 + rst.linker >= 1.9 + furo + sphinx-lint + + # local + +[options.entry_points] diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..1093e028 --- /dev/null +++ b/tox.ini @@ -0,0 +1,49 @@ +[tox] +toxworkdir={env:TOX_WORK_DIR:.tox} + + +[testenv] +deps = +setenv = + PYTHONWARNDEFAULTENCODING = 1 +commands = + pytest {posargs} +usedevelop = True +extras = + testing + +[testenv:docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint + +[testenv:finalize] +skip_install = True +deps = + towncrier + jaraco.develop >= 7.23 +passenv = * +commands = + python -m jaraco.develop.finalize + + +[testenv:release] +skip_install = True +deps = + build + twine>=3 + jaraco.develop>=7.1 +passenv = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + python -m twine upload dist/* + python -m jaraco.develop.create-github-release From 972d1b3033afba89ffa20e6c492c4d02742e8a9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jul 2023 22:20:28 -0400 Subject: [PATCH 620/835] Add links to project home page and pypi. Fixes jaraco/skeleton#77. --- docs/index.rst | 4 ++++ setup.cfg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 53117d16..5a3c6770 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,10 @@ Welcome to |project| documentation! =================================== +.. sidebar-links:: + :home: + :pypi: + .. toctree:: :maxdepth: 1 diff --git a/setup.cfg b/setup.cfg index a9ca2a88..46f7bdf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ testing = docs = # upstream sphinx >= 3.5 - jaraco.packaging >= 9 + jaraco.packaging >= 9.3 rst.linker >= 1.9 furo sphinx-lint From 747c2a36524f83b84a3d9497121313bb5751b877 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 08:59:49 -0400 Subject: [PATCH 621/835] Replace redundant step names with simple 'Run'. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f54dfbc6..b8224099 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox docs: @@ -82,7 +82,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox check: # This job does nothing and is only used for the branch protection @@ -117,7 +117,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Release + - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} From d09b7d664783e66d6181b427e5d6eb131527c258 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 11:33:14 -0400 Subject: [PATCH 622/835] Fix failing test after changelog rename. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index d61bb3c8..ff26745e 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -641,7 +641,7 @@ def chunks(self, size, *args, **kwargs): :example: >>> hash = hashlib.md5() - >>> for chunk in Path("CHANGES.rst").chunks(8192, mode='rb'): + >>> for chunk in Path("NEWS.rst").chunks(8192, mode='rb'): ... hash.update(chunk) This will read the file by chunks of 8192 bytes. From 5698f1e471aa345362ab92735fd8c052ac275468 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 12:03:09 -0400 Subject: [PATCH 623/835] Add examples illustrating symbolic chmod. --- path/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/path/__init__.py b/path/__init__.py index ff26745e..2cfa0961 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1105,6 +1105,12 @@ def chmod(self, mode): Set the mode. May be the new mode (os.chmod behavior) or a `symbolic mode `_. + >>> a_file = Path(getfixture('tmp_path')).joinpath('afile.txt').touch() + >>> a_file.chmod(0o700) + Path(... + >>> a_file.chmod('u+x') + Path(... + .. seealso:: :func:`os.chmod` """ if isinstance(mode, str): From 1bcceaaec967babca438a895261135fca7d41cfa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:15:57 -0400 Subject: [PATCH 624/835] Remove Python 3.7 compatibility. --- path/__init__.py | 6 +-- path/py37compat.py | 125 -------------------------------------------- path/py37compat.pyi | 17 ------ 3 files changed, 2 insertions(+), 146 deletions(-) delete mode 100644 path/py37compat.py delete mode 100644 path/py37compat.pyi diff --git a/path/__init__.py b/path/__init__.py index 2cfa0961..eb4aceb1 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -53,7 +53,6 @@ from . import matchers from . import masks from . import classes -from .py37compat import best_realpath, lru_cache __all__ = ['Path', 'TempDir'] @@ -151,7 +150,7 @@ def __init__(self, other=''): self._validate() @classmethod - @lru_cache + @functools.lru_cache def using_module(cls, module): subclass_name = cls.__name__ + '_' + module.__name__ bases = (cls,) @@ -239,8 +238,7 @@ def normpath(self): def realpath(self): """.. seealso:: :func:`os.path.realpath`""" - realpath = best_realpath(self.module) - return self._next_class(realpath(self)) + return self._next_class(self.module.realpath(self)) def expanduser(self): """.. seealso:: :func:`os.path.expanduser`""" diff --git a/path/py37compat.py b/path/py37compat.py deleted file mode 100644 index f2a9e8b4..00000000 --- a/path/py37compat.py +++ /dev/null @@ -1,125 +0,0 @@ -import functools -import os - - -def best_realpath(module): - """ - Given a path module (i.e. ntpath, posixpath), - determine the best 'realpath' function to use - for best future compatibility. - """ - needs_backport = module.realpath is module.abspath - return realpath_backport if needs_backport else module.realpath - - -# backport taken from jaraco.windows 5 -def realpath_backport(path): - if isinstance(path, str): - prefix = '\\\\?\\' - unc_prefix = prefix + 'UNC' - new_unc_prefix = '\\' - cwd = os.getcwd() - else: - prefix = b'\\\\?\\' - unc_prefix = prefix + b'UNC' - new_unc_prefix = b'\\' - cwd = os.getcwdb() - had_prefix = path.startswith(prefix) - path, ok = _resolve_path(cwd, path, {}) - # The path returned by _getfinalpathname will always start with \\?\ - - # strip off that prefix unless it was already provided on the original - # path. - if not had_prefix: - # For UNC paths, the prefix will actually be \\?\UNC - handle that - # case as well. - if path.startswith(unc_prefix): - path = new_unc_prefix + path[len(unc_prefix) :] - elif path.startswith(prefix): - path = path[len(prefix) :] - return path - - -def _resolve_path(path, rest, seen): # noqa: C901 - # Windows normalizes the path before resolving symlinks; be sure to - # follow the same behavior. - rest = os.path.normpath(rest) - - if isinstance(rest, str): - sep = '\\' - else: - sep = b'\\' - - if os.path.isabs(rest): - drive, rest = os.path.splitdrive(rest) - path = drive + sep - rest = rest[1:] - - while rest: - name, _, rest = rest.partition(sep) - new_path = os.path.join(path, name) if path else name - if os.path.exists(new_path): - if not rest: - # The whole path exists. Resolve it using the OS. - path = os.path._getfinalpathname(new_path) - else: - # The OS can resolve `new_path`; keep traversing the path. - path = new_path - elif not os.path.lexists(new_path): - # `new_path` does not exist on the filesystem at all. Use the - # OS to resolve `path`, if it exists, and then append the - # remainder. - if os.path.exists(path): - path = os.path._getfinalpathname(path) - rest = os.path.join(name, rest) if rest else name - return os.path.join(path, rest), True - else: - # We have a symbolic link that the OS cannot resolve. Try to - # resolve it ourselves. - - # On Windows, symbolic link resolution can be partially or - # fully disabled [1]. The end result of a disabled symlink - # appears the same as a broken symlink (lexists() returns True - # but exists() returns False). And in both cases, the link can - # still be read using readlink(). Call stat() and check the - # resulting error code to ensure we don't circumvent the - # Windows symbolic link restrictions. - # [1] https://technet.microsoft.com/en-us/library/cc754077.aspx - try: - os.stat(new_path) - except OSError as e: - # WinError 1463: The symbolic link cannot be followed - # because its type is disabled. - if e.winerror == 1463: - raise - - key = os.path.normcase(new_path) - if key in seen: - # This link has already been seen; try to use the - # previously resolved value. - path = seen[key] - if path is None: - # It has not yet been resolved, which means we must - # have a symbolic link loop. Return what we have - # resolved so far plus the remainder of the path (who - # cares about the Zen of Python?). - path = os.path.join(new_path, rest) if rest else new_path - return path, False - else: - # Mark this link as in the process of being resolved. - seen[key] = None - # Try to resolve it. - path, ok = _resolve_path(path, os.readlink(new_path), seen) - if ok: - # Resolution succeded; store the resolved value. - seen[key] = path - else: - # Resolution failed; punt. - return (os.path.join(path, rest) if rest else path), False - return path, True - - -def lru_cache(user_function): - """ - Support for lru_cache(user_function) - """ - return functools.lru_cache()(user_function) diff --git a/path/py37compat.pyi b/path/py37compat.pyi deleted file mode 100644 index ea62fa06..00000000 --- a/path/py37compat.pyi +++ /dev/null @@ -1,17 +0,0 @@ -import os - -from types import ModuleType -from typing import Any, AnyStr, Callable, Dict, Tuple, overload - -def best_realpath(module: ModuleType) -> Callable[[AnyStr], AnyStr]: ... -@overload -def realpath_backport(path: str) -> str: ... -@overload -def realpath_backport(path: bytes) -> bytes: ... -@overload -def _resolve_path(path: str, rest: str, seen: Dict[Any, Any]) -> Tuple[str, bool]: ... -@overload -def _resolve_path( - path: bytes, rest: bytes, seen: Dict[Any, Any] -) -> Tuple[bytes, bool]: ... -def lru_cache(user_function: Callable[..., Any]) -> Callable[..., Any]: ... From 0fd55a470d6453e84a582964108b4f2fd495fbc3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:17:22 -0400 Subject: [PATCH 625/835] Ran pyupgrade for Python 3.8+ followed by ruff --fix and black --- path/__init__.py | 23 ++++++++++------------- path/__init__.pyi | 1 - test_path.py | 37 ++++++++++++++++++------------------- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index eb4aceb1..10681848 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -34,7 +34,6 @@ import functools import re import contextlib -import io import importlib import itertools import datetime @@ -168,11 +167,11 @@ def _next_class(cls): # --- Special Python methods. def __repr__(self): - return '%s(%s)' % (type(self).__name__, super(Path, self).__repr__()) + return '{}({})'.format(type(self).__name__, super().__repr__()) # Adding a Path and a string yields a Path. def __add__(self, more): - return self._next_class(super(Path, self).__add__(more)) + return self._next_class(super().__add__(more)) def __radd__(self, other): return self._next_class(other.__add__(self)) @@ -296,7 +295,7 @@ def with_suffix(self, suffix): ValueError: Invalid suffix 'zip' """ if not suffix.startswith('.'): - raise ValueError("Invalid suffix {suffix!r}".format(**locals())) + raise ValueError(f"Invalid suffix {suffix!r}") return self.stripext() + suffix @@ -549,8 +548,7 @@ def walk(self, match=None, errors='strict'): continue if do_traverse: - for item in child.walk(errors=errors, match=match): - yield item + yield from child.walk(errors=errors, match=match) def walkdirs(self, *args, **kwargs): """Iterator over subdirs, recursively.""" @@ -623,7 +621,7 @@ def open(self, *args, **kwargs): Keyword arguments work as in :func:`io.open`. If the file cannot be opened, an :class:`OSError` is raised. """ - return io.open(self, *args, **kwargs) + return open(self, *args, **kwargs) def bytes(self): """Open this file, read all bytes, return them as a string.""" @@ -645,8 +643,7 @@ def chunks(self, size, *args, **kwargs): This will read the file by chunks of 8192 bytes. """ with self.open(*args, **kwargs) as f: - for chunk in iter(lambda: f.read(size) or None, None): - yield chunk + yield from iter(lambda: f.read(size) or None, None) def write_bytes(self, bytes, append=False): """Open this file and write the given bytes to it. @@ -1402,7 +1399,7 @@ def in_place( backup_fn = self + (backup_extension or os.extsep + 'bak') backup_fn.remove_p() self.rename(backup_fn) - readable = io.open( + readable = open( backup_fn, mode, buffering=buffering, @@ -1424,7 +1421,7 @@ def in_place( os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC os_mode |= getattr(os, 'O_BINARY', 0) fd = os.open(self, os_mode, perm) - writable = io.open( + writable = open( fd, "w" + mode.replace('r', ''), buffering=buffering, @@ -1556,7 +1553,7 @@ def get_dir(self, scope, class_): Return the callable function from appdirs, but with the result wrapped in self.path_class """ - prop_name = '{scope}_{class_}_dir'.format(**locals()) + prop_name = f'{scope}_{class_}_dir' value = getattr(self.wrapper, prop_name) MultiPath = Multi.for_class(self.path_class) return MultiPath.detect(value) @@ -1617,7 +1614,7 @@ def _next_class(cls): def __new__(cls, *args, **kwargs): dirname = tempfile.mkdtemp(*args, **kwargs) - return super(TempDir, cls).__new__(cls, dirname) + return super().__new__(cls, dirname) def __init__(self, *args, **kwargs): pass diff --git a/path/__init__.pyi b/path/__init__.pyi index a0b8f561..1031f3e2 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -3,7 +3,6 @@ from __future__ import annotations import builtins import contextlib import os -import shutil import sys from io import ( BufferedRandom, diff --git a/test_path.py b/test_path.py index dc7b651d..364aeac2 100644 --- a/test_path.py +++ b/test_path.py @@ -13,7 +13,6 @@ time on files. """ -import io import os import sys import shutil @@ -94,7 +93,7 @@ def test_string_compatibility(self): """Test compatibility with ordinary strings.""" x = Path('xyzzy') assert x == 'xyzzy' - assert x == str('xyzzy') + assert x == 'xyzzy' # sorting items = [Path('fhj'), Path('fgh'), 'E', Path('d'), 'A', Path('B'), 'c'] @@ -316,7 +315,7 @@ def get_command_time(cmd): args = [sys.executable, '-m', 'timeit', '-n', '1', '-r', '1', '-u', 'usec'] + [ cmd ] - res = subprocess.check_output(args, universal_newlines=True) + res = subprocess.check_output(args, text=True) dur = re.search(r'(\d+) usec per loop', res).group(1) return datetime.timedelta(microseconds=int(dur)) @@ -474,7 +473,7 @@ def test_touch(self, tmpdir): time.sleep(threshold * 2) fobj = open(f, 'ab') - fobj.write('some bytes'.encode('utf-8')) + fobj.write(b'some bytes') fobj.close() time.sleep(threshold * 2) @@ -550,7 +549,7 @@ def test_listing(self, tmpdir): @pytest.fixture def bytes_filename(self, tmpdir): - name = r'r\xe9\xf1emi'.encode('latin-1') + name = br'r\xe9\xf1emi' base = str(tmpdir).encode('ascii') try: with open(os.path.join(base, name), 'wb'): @@ -739,7 +738,7 @@ def test_unicode(self, tmpdir, encoding): stripped = [line.replace('\n', '') for line in expectedLines] # write bytes manually to file - with io.open(p, 'wb') as strm: + with open(p, 'wb') as strm: strm.write(given.encode(encoding)) # test all 3 path read-fully functions, including @@ -913,18 +912,20 @@ def check_link(self): def test_with_nonexisting_dst_kwargs(self): self.subdir_a.merge_tree(self.subdir_b, symlinks=True) assert self.subdir_b.isdir() - expected = set( - (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) - ) + expected = { + self.subdir_b / self.test_file.name, + self.subdir_b / self.test_link.name, + } assert set(self.subdir_b.listdir()) == expected self.check_link() def test_with_nonexisting_dst_args(self): self.subdir_a.merge_tree(self.subdir_b, True) assert self.subdir_b.isdir() - expected = set( - (self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name) - ) + expected = { + self.subdir_b / self.test_file.name, + self.subdir_b / self.test_link.name, + } assert set(self.subdir_b.listdir()) == expected self.check_link() @@ -941,13 +942,11 @@ def test_with_existing_dst(self): self.subdir_a.merge_tree(self.subdir_b, True) assert self.subdir_b.isdir() - expected = set( - ( - self.subdir_b / self.test_file.name, - self.subdir_b / self.test_link.name, - self.subdir_b / test_new.name, - ) - ) + expected = { + self.subdir_b / self.test_file.name, + self.subdir_b / self.test_link.name, + self.subdir_b / test_new.name, + } assert set(self.subdir_b.listdir()) == expected self.check_link() assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 From 03f5fd7ba527b9341953108990783dcc8fdeef14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 18:58:39 -0400 Subject: [PATCH 626/835] Add permissions attribute Fixes #211 --- newsfragments/211.feature.rst | 1 + path/__init__.py | 15 ++++++++++++++ path/masks.py | 39 +++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 newsfragments/211.feature.rst diff --git a/newsfragments/211.feature.rst b/newsfragments/211.feature.rst new file mode 100644 index 00000000..779f6cd2 --- /dev/null +++ b/newsfragments/211.feature.rst @@ -0,0 +1 @@ +Added ``.permissions`` attribute. diff --git a/path/__init__.py b/path/__init__.py index 10681848..c5061ed5 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -990,6 +990,21 @@ def getsize(self): """, ) + @property + def permissions(self) -> masks.Permissions: + """ + Permissions. + + >>> perms = Path('.').permissions + >>> isinstance(perms, int) + True + >>> set(perms.symbolic) <= set('rwx-') + True + >>> perms.symbolic + 'r...' + """ + return masks.Permissions(self.stat().st_mode) + def access(self, *args, **kwargs): """ Return does the real user have access to this path. diff --git a/path/masks.py b/path/masks.py index 761e51f8..77660274 100644 --- a/path/masks.py +++ b/path/masks.py @@ -1,6 +1,7 @@ import re import functools import operator +import itertools # from jaraco.functools @@ -9,6 +10,19 @@ def compose(*funcs): return functools.reduce(compose_two, funcs) +# from jaraco.structures.binary +def gen_bit_values(number): + """ + Return a zero or one for each bit of a numeric value up to the most + significant 1 bit, beginning with the least significant bit. + + >>> list(gen_bit_values(16)) + [0, 0, 0, 0, 1] + """ + digits = bin(number)[2:] + return map(int, reversed(digits)) + + def compound(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. @@ -83,3 +97,28 @@ def simple(mode): '=': lambda mask, target: target & retain ^ mask, } return functools.partial(op_map[op], mask) + + +class Permissions(int): + """ + >>> perms = Permissions(0o764) + >>> oct(perms) + '0o764' + >>> perms.symbolic + 'rwxrw-r--' + >>> str(perms) + 'rwxrw-r--' + """ + + @property + def symbolic(self): + return ''.join( + ['-', val][bit] for val, bit in zip(itertools.cycle('rwx'), self.bits) + ) + + @property + def bits(self): + return reversed(tuple(gen_bit_values(self))) + + def __str__(self): + return self.symbolic From 66c8749bb8dbb9c39d844802be158a0b52971243 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:11:42 -0400 Subject: [PATCH 627/835] Pad to 9 bits in case user read is not set. --- path/masks.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/path/masks.py b/path/masks.py index 77660274..e7037e96 100644 --- a/path/masks.py +++ b/path/masks.py @@ -23,6 +23,39 @@ def gen_bit_values(number): return map(int, reversed(digits)) +# from more_itertools +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*:: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + """ + it = iter(iterable) + if n is None: + yield from itertools.chain(it, itertools.repeat(fillvalue)) + elif n < 1: + raise ValueError('n must be at least 1') + else: + item_count = 0 + for item in it: + yield item + item_count += 1 + + remaining = (n - item_count) % n if next_multiple else n - item_count + for _ in range(remaining): + yield fillvalue + + def compound(mode): """ Support multiple, comma-separated Unix chmod symbolic modes. @@ -108,6 +141,8 @@ class Permissions(int): 'rwxrw-r--' >>> str(perms) 'rwxrw-r--' + >>> str(Permissions(0o222)) + '-w--w--w-' """ @property @@ -118,7 +153,7 @@ def symbolic(self): @property def bits(self): - return reversed(tuple(gen_bit_values(self))) + return reversed(tuple(padded(gen_bit_values(self), 0, n=9))) def __str__(self): return self.symbolic From de97b1d281cd223d7c3b5ddcd4fce26d4a7a2584 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:21:00 -0400 Subject: [PATCH 628/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+drop-py37.feature.rst | 1 - newsfragments/211.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+drop-py37.feature.rst delete mode 100644 newsfragments/211.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 63ecb163..e9216a5f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v16.7.0 +======= + +Features +-------- + +- Added ``.permissions`` attribute. (#211) +- Require Python 3.8 or later. + + v16.6.0 ------- diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. diff --git a/newsfragments/211.feature.rst b/newsfragments/211.feature.rst deleted file mode 100644 index 779f6cd2..00000000 --- a/newsfragments/211.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``.permissions`` attribute. From fd4ad316189d69b3d9dd9459b5c88df0dce97e23 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:22:41 -0400 Subject: [PATCH 629/835] Set stacklevel=2 Fixes #210 --- newsfragments/210.bugfix.rst | 1 + path/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 newsfragments/210.bugfix.rst diff --git a/newsfragments/210.bugfix.rst b/newsfragments/210.bugfix.rst new file mode 100644 index 00000000..e9d16f86 --- /dev/null +++ b/newsfragments/210.bugfix.rst @@ -0,0 +1 @@ +Set ``stacklevel=2`` in deprecation warning for ``.text``. diff --git a/path/__init__.py b/path/__init__.py index c5061ed5..5aae5f79 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -674,7 +674,11 @@ def text(self, encoding=None, errors='strict'): Converts all newline sequences to ``\n``. """ - warnings.warn(".text is deprecated; use read_text", DeprecationWarning) + warnings.warn( + ".text is deprecated; use read_text", + DeprecationWarning, + stacklevel=2, + ) return U_NEWLINE.sub('\n', self.read_text(encoding, errors)) def write_text( From 095d7bea8861c82858bcc0e8e8a4d68b571323fc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:40:09 -0400 Subject: [PATCH 630/835] Extract method to more narrowly filter the warning. --- test_path.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test_path.py b/test_path.py index 364aeac2..938132f1 100644 --- a/test_path.py +++ b/test_path.py @@ -300,12 +300,15 @@ def test_removedirs_p(self, tmpdir): class TestReadWriteText: - @pytest.mark.filterwarnings('ignore:Writing bytes in write_text') def test_read_write(self, tmpdir): file = path.Path(tmpdir) / 'filename' file.write_text('hello world') assert file.read_text() == 'hello world' assert file.read_bytes() == b'hello world' + + @pytest.mark.filterwarnings('ignore:Writing bytes in write_text') + def test_write_text_bytes(self, tmpdir): + file = path.Path(tmpdir) / 'filename' file.write_text(b'hello world') From a069c5e2ae0f6a51404b6dba33be24505c211f77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 19:40:41 -0400 Subject: [PATCH 631/835] Fix EncodingWarnings --- test_path.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test_path.py b/test_path.py index 938132f1..c3ee6f96 100644 --- a/test_path.py +++ b/test_path.py @@ -302,8 +302,8 @@ def test_removedirs_p(self, tmpdir): class TestReadWriteText: def test_read_write(self, tmpdir): file = path.Path(tmpdir) / 'filename' - file.write_text('hello world') - assert file.read_text() == 'hello world' + file.write_text('hello world', encoding='utf-8') + assert file.read_text(encoding='utf-8') == 'hello world' assert file.read_bytes() == b'hello world' @pytest.mark.filterwarnings('ignore:Writing bytes in write_text') @@ -318,7 +318,7 @@ def get_command_time(cmd): args = [sys.executable, '-m', 'timeit', '-n', '1', '-r', '1', '-u', 'usec'] + [ cmd ] - res = subprocess.check_output(args, text=True) + res = subprocess.check_output(args, text=True, encoding='utf-8') dur = re.search(r'(\d+) usec per loop', res).group(1) return datetime.timedelta(microseconds=int(dur)) @@ -346,7 +346,7 @@ def test_link(self, tmpdir): target = Path(tmpdir) / 'target' target.write_text('hello', encoding='utf-8') link = target.link(Path(tmpdir) / 'link') - assert link.read_text() == 'hello' + assert link.read_text(encoding='utf-8') == 'hello' def test_symlink_none(self, tmpdir): root = Path(tmpdir) @@ -537,7 +537,7 @@ def test_listing(self, tmpdir): # Try a test with 20 files files = [d / ('%d.txt' % i) for i in range(20)] for f in files: - fobj = open(f, 'w') + fobj = open(f, 'w', encoding='utf-8') fobj.write('some text\n') fobj.close() try: @@ -638,7 +638,7 @@ def test_shutil(self, tmpdir): testA.mkdir() testB.mkdir() - f = open(testFile, 'w') + f = open(testFile, 'w', encoding='utf-8') f.write('x' * 10000) f.close() @@ -822,8 +822,8 @@ def test_chunks(self, tmpdir): p = (TempDir() / 'test.txt').touch() txt = "0123456789" size = 5 - p.write_text(txt) - for i, chunk in enumerate(p.chunks(size)): + p.write_text(txt, encoding='utf-8') + for i, chunk in enumerate(p.chunks(size, encoding='utf-8')): assert chunk == txt[i * size : i * size + size] assert i == len(txt) / size - 1 @@ -902,7 +902,7 @@ def testing_structure(self, tmpdir): self.subdir_a.mkdir() self.subdir_b.mkdir() - with open(self.test_file, 'w') as f: + with open(self.test_file, 'w', encoding='utf-8') as f: f.write('x' * 10000) self.test_file.symlink(self.test_link) @@ -939,7 +939,7 @@ def test_with_existing_dst(self): self.test_link.remove() test_new = self.subdir_a / 'newfile.txt' test_new.touch() - with open(self.test_file, 'w') as f: + with open(self.test_file, 'w', encoding='utf-8') as f: f.write('x' * 5000) self.subdir_a.merge_tree(self.subdir_b, True) @@ -971,11 +971,11 @@ def test_only_newer(self): newer copies in the dest. """ target = self.subdir_b / 'testfile.txt' - target.write_text('this is newer') + target.write_text('this is newer', encoding='utf-8') self.subdir_a.merge_tree( self.subdir_b, copy_function=path.only_newer(shutil.copy2) ) - assert target.read_text() == 'this is newer' + assert target.read_text(encoding='utf-8') == 'this is newer' class TestChdir: @@ -1187,29 +1187,29 @@ class TestInPlace: @classmethod def create_reference(cls, tmpdir): p = Path(tmpdir) / 'document' - with p.open('w') as stream: + with p.open('w', encoding='utf-8') as stream: stream.write(cls.reference_content) return p def test_line_by_line_rewrite(self, tmpdir): doc = self.create_reference(tmpdir) # reverse all the text in the document, line by line - with doc.in_place() as (reader, writer): + with doc.in_place(encoding='utf-8') as (reader, writer): for line in reader: r_line = ''.join(reversed(line.strip())) + '\n' writer.write(r_line) - with doc.open() as stream: + with doc.open(encoding='utf-8') as stream: data = stream.read() assert data == self.reversed_content def test_exception_in_context(self, tmpdir): doc = self.create_reference(tmpdir) with pytest.raises(RuntimeError) as exc: - with doc.in_place() as (reader, writer): + with doc.in_place(encoding='utf-8') as (reader, writer): writer.write(self.alternate_content) raise RuntimeError("some error") assert "some error" in str(exc.value) - with doc.open() as stream: + with doc.open(encoding='utf-8') as stream: data = stream.read() assert 'Lorem' not in data assert 'lazy dog' in data From 823de5c49bea98f0d18cb242903d073f2ab33afb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:11:53 -0400 Subject: [PATCH 632/835] Use context managers to close --- test_path.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test_path.py b/test_path.py index c3ee6f96..2d9ff31f 100644 --- a/test_path.py +++ b/test_path.py @@ -475,9 +475,8 @@ def test_touch(self, tmpdir): assert t0 <= ct <= t1 time.sleep(threshold * 2) - fobj = open(f, 'ab') - fobj.write(b'some bytes') - fobj.close() + with open(f, 'ab') as fobj: + fobj.write(b'some bytes') time.sleep(threshold * 2) t2 = time.time() - threshold @@ -537,9 +536,8 @@ def test_listing(self, tmpdir): # Try a test with 20 files files = [d / ('%d.txt' % i) for i in range(20)] for f in files: - fobj = open(f, 'w', encoding='utf-8') - fobj.write('some text\n') - fobj.close() + with open(f, 'w', encoding='utf-8') as fobj: + fobj.write('some text\n') try: files2 = d.listdir() files.sort() @@ -638,9 +636,8 @@ def test_shutil(self, tmpdir): testA.mkdir() testB.mkdir() - f = open(testFile, 'w', encoding='utf-8') - f.write('x' * 10000) - f.close() + with open(testFile, 'w', encoding='utf-8') as f: + f.write('x' * 10000) # Test simple file copying. testFile.copyfile(testCopy) From 309c4b041abef2fb257e9ebfa18dc590f6a77c91 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:12:52 -0400 Subject: [PATCH 633/835] Inline call --- path/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 5aae5f79..eebdc3a0 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1221,8 +1221,7 @@ def touch(self): """Set the access/modified times of this file to the current time. Create the file if it does not exist. """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) - os.close(fd) + os.close(os.open(self, os.O_WRONLY | os.O_CREAT, 0o666)) os.utime(self, None) return self From e24005e0ec87b22d851e67ac4c7cb4c90f4d89fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:18:51 -0400 Subject: [PATCH 634/835] Add test for nesting. Closes coverage gap. --- test_path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_path.py b/test_path.py index 2d9ff31f..d1053692 100644 --- a/test_path.py +++ b/test_path.py @@ -974,6 +974,11 @@ def test_only_newer(self): ) assert target.read_text(encoding='utf-8') == 'this is newer' + def test_nested(self): + self.subdir_a.joinpath('subsub').mkdir() + self.subdir_a.merge_tree(self.subdir_b) + assert self.subdir_b.joinpath('subsub').isdir() + class TestChdir: def test_chdir_or_cd(self, tmpdir): From 0db600ae4a1d1cbf07a0a71982241e926ed01a61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:25:41 -0400 Subject: [PATCH 635/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/210.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/210.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index e9216a5f..4efbedb5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.7.1 +======= + +Bugfixes +-------- + +- Set ``stacklevel=2`` in deprecation warning for ``.text``. (#210) + + v16.7.0 ======= diff --git a/newsfragments/210.bugfix.rst b/newsfragments/210.bugfix.rst deleted file mode 100644 index e9d16f86..00000000 --- a/newsfragments/210.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Set ``stacklevel=2`` in deprecation warning for ``.text``. From a9a238e5c7c7c32b48a5d584ef1483d3547b4bf6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:37:04 -0400 Subject: [PATCH 636/835] Remove unreachable and duplicate code. Replace os.fstat with os.stat (now equivalent). Brings coverage to 100%. --- path/__init__.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index eebdc3a0..7d46d1d2 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1425,30 +1425,22 @@ def in_place( errors=errors, newline=newline, ) - try: - perm = os.fstat(readable.fileno()).st_mode - except OSError: - writable = self.open( - 'w' + mode.replace('r', ''), - buffering=buffering, - encoding=encoding, - errors=errors, - newline=newline, - ) - else: - os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC - os_mode |= getattr(os, 'O_BINARY', 0) - fd = os.open(self, os_mode, perm) - writable = open( - fd, - "w" + mode.replace('r', ''), - buffering=buffering, - encoding=encoding, - errors=errors, - newline=newline, - ) - with contextlib.suppress(OSError, AttributeError): - self.chmod(perm) + + perm = os.stat(readable.fileno()).st_mode + os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC + os_mode |= getattr(os, 'O_BINARY', 0) + fd = os.open(self, os_mode, perm) + writable = open( + fd, + "w" + mode.replace('r', ''), + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + ) + with contextlib.suppress(OSError, AttributeError): + self.chmod(perm) + try: yield readable, writable except Exception: From dfa223a15b899f39c34504511918b00da3cd9cb1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 20:54:40 -0400 Subject: [PATCH 637/835] Clean up the README, removing stale guidance. --- README.rst | 57 +++++++++++------------------------------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index 69aa8737..87620c33 100644 --- a/README.rst +++ b/README.rst @@ -72,18 +72,16 @@ One may have imagined ``pathlib`` would supersede ``path``. But the implementation and the usage quickly diverge, and ``path`` has several advantages over ``pathlib``: -- ``path`` implements ``Path`` objects as a subclass of - ``str``, and as a result these ``Path`` - objects may be passed directly to other APIs that expect simple - text representations of paths, whereas with ``pathlib``, one - must first cast values to strings before passing them to - APIs unaware of ``pathlib``. This shortcoming was `addressed - by PEP 519 `_, - in Python 3.6. -- ``path`` goes beyond exposing basic functionality of a path - and exposes commonly-used behaviors on a path, providing - methods like ``rmtree`` (from shlib) and ``remove_p`` (remove - a file if it exists). +- ``path`` implements ``Path`` objects as a subclass of ``str``, and as a + result these ``Path`` objects may be passed directly to other APIs that + expect simple text representations of paths, whereas with ``pathlib``, one + must first cast values to strings before passing them to APIs unaware of + ``pathlib``. This shortcoming was somewhat `mitigated by PEP 519 + `_, in Python 3.6. +- ``path`` give quality of life features beyond exposing basic functionality + of a path. ``path`` provides methods like ``rmtree`` (from shlib) and + ``remove_p`` (remove a file if it exists), properties like ``.permissions``, + and sophisticated ``walk``, ``TempDir``, and ``chmod`` behaviors. - As a PyPI-hosted package, ``path`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. @@ -104,40 +102,6 @@ object is a drop-in replacement for ``pathlib.Path*`` objects. This project welcomes contributions to improve that compatibility where it's lacking. -Alternatives -============ - -In addition to -`pathlib `_, the -`pylib project `_ implements a -`LocalPath `_ -class, which shares some behaviors and interfaces with ``path``. - -Development -=========== - -To install a development version, use the Github links to clone or -download a snapshot of the latest code. Alternatively, if you have git -installed, you may be able to use ``pip`` to install directly from -the repository:: - - pip install git+https://github.com/jaraco/path.git - -Testing -======= - -Tests are invoked with `tox `_. After -having installed tox, simply invoke ``tox`` in a checkout of the repo -to invoke the tests. - -Tests are also run in continuous integration. See the badges above -for links to the CI runs. - -Releasing -========= - -Tagged releases are automatically published to PyPI by Azure -Pipelines, assuming the tests pass. Origins ======= @@ -146,6 +110,7 @@ The ``path.py`` project was initially released in 2003 by Jason Orendorff and has been continuously developed and supported by several maintainers over the years. + For Enterprise ============== From da7aa650c6e57e9246ad165e44b77ac210cb8486 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 21:10:11 -0400 Subject: [PATCH 638/835] Move type annotations into masks.py --- path/masks.py | 9 ++++++--- path/masks.pyi | 5 ----- 2 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 path/masks.pyi diff --git a/path/masks.py b/path/masks.py index e7037e96..13ae00a3 100644 --- a/path/masks.py +++ b/path/masks.py @@ -5,7 +5,10 @@ # from jaraco.functools -def compose(*funcs): +from typing import Any, Callable + + +def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa return functools.reduce(compose_two, funcs) @@ -56,7 +59,7 @@ def padded(iterable, fillvalue=None, n=None, next_multiple=False): yield fillvalue -def compound(mode): +def compound(mode: str) -> Callable[[int], int]: """ Support multiple, comma-separated Unix chmod symbolic modes. @@ -66,7 +69,7 @@ def compound(mode): return compose(*map(simple, reversed(mode.split(',')))) -def simple(mode): +def simple(mode: str) -> Callable[[int], int]: """ Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function suitable for applying to a mask to affect that change. diff --git a/path/masks.pyi b/path/masks.pyi deleted file mode 100644 index d69bf202..00000000 --- a/path/masks.pyi +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Any, Callable - -def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: ... -def compound(mode: str) -> Callable[[int], int]: ... -def simple(mode: str) -> Callable[[int], int]: ... From 7a15cd167a774c37f18881aecdf46ce31d355fb0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 21:18:35 -0400 Subject: [PATCH 639/835] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.pyi | 612 ++++++++++++++++++++++++++++++++++------------ path/classes.pyi | 12 +- path/matchers.pyi | 32 ++- 3 files changed, 492 insertions(+), 164 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index 1031f3e2..09df8e7c 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -45,99 +45,203 @@ from . import classes # Type for the match argument for several methods _Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] + class TreeWalkWarning(Warning): pass + class Traversal: follow: Callable[[Path], bool] - def __init__(self, follow: Callable[[Path], bool]): ... + + def __init__(self, follow: Callable[[Path], bool]): + ... + def __call__( self, walker: Generator[Path, Optional[Callable[[], bool]], None], - ) -> Iterator[Path]: ... + ) -> Iterator[Path]: + ... + class Path(str): module: Any - def __init__(self, other: Any = ...) -> None: ... + + def __init__(self, other: Any = ...) -> None: + ... + @classmethod - def using_module(cls, module: ModuleType) -> Type[Path]: ... + def using_module(cls, module: ModuleType) -> Type[Path]: + ... + @classes.ClassProperty @classmethod - def _next_class(cls: Type[Self]) -> Type[Self]: ... - def __repr__(self) -> str: ... - def __add__(self: Self, more: str) -> Self: ... - def __radd__(self: Self, other: str) -> Self: ... - def __div__(self: Self, rel: str) -> Self: ... - def __truediv__(self: Self, rel: str) -> Self: ... - def __rdiv__(self: Self, rel: str) -> Self: ... - def __rtruediv__(self: Self, rel: str) -> Self: ... - def __enter__(self: Self) -> Self: ... + def _next_class(cls: Type[Self]) -> Type[Self]: + ... + + def __repr__(self) -> str: + ... + + def __add__(self: Self, more: str) -> Self: + ... + + def __radd__(self: Self, other: str) -> Self: + ... + + def __div__(self: Self, rel: str) -> Self: + ... + + def __truediv__(self: Self, rel: str) -> Self: + ... + + def __rdiv__(self: Self, rel: str) -> Self: + ... + + def __rtruediv__(self: Self, rel: str) -> Self: + ... + + def __enter__(self: Self) -> Self: + ... + def __exit__( self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: ... + ) -> None: + ... + @classmethod - def getcwd(cls: Type[Self]) -> Self: ... - def abspath(self: Self) -> Self: ... - def normcase(self: Self) -> Self: ... - def normpath(self: Self) -> Self: ... - def realpath(self: Self) -> Self: ... - def expanduser(self: Self) -> Self: ... - def expandvars(self: Self) -> Self: ... - def dirname(self: Self) -> Self: ... - def basename(self: Self) -> Self: ... - def expand(self: Self) -> Self: ... + def getcwd(cls: Type[Self]) -> Self: + ... + + def abspath(self: Self) -> Self: + ... + + def normcase(self: Self) -> Self: + ... + + def normpath(self: Self) -> Self: + ... + + def realpath(self: Self) -> Self: + ... + + def expanduser(self: Self) -> Self: + ... + + def expandvars(self: Self) -> Self: + ... + + def dirname(self: Self) -> Self: + ... + + def basename(self: Self) -> Self: + ... + + def expand(self: Self) -> Self: + ... + @property - def stem(self) -> str: ... + def stem(self) -> str: + ... + @property - def ext(self) -> str: ... - def with_suffix(self: Self, suffix: str) -> Self: ... + def ext(self) -> str: + ... + + def with_suffix(self: Self, suffix: str) -> Self: + ... + @property - def drive(self: Self) -> Self: ... + def drive(self: Self) -> Self: + ... + @property - def parent(self: Self) -> Self: ... + def parent(self: Self) -> Self: + ... + @property - def name(self: Self) -> Self: ... - def splitpath(self: Self) -> Tuple[Self, str]: ... - def splitdrive(self: Self) -> Tuple[Self, Self]: ... - def splitext(self: Self) -> Tuple[Self, str]: ... - def stripext(self: Self) -> Self: ... + def name(self: Self) -> Self: + ... + + def splitpath(self: Self) -> Tuple[Self, str]: + ... + + def splitdrive(self: Self) -> Tuple[Self, Self]: + ... + + def splitext(self: Self) -> Tuple[Self, str]: + ... + + def stripext(self: Self) -> Self: + ... + @classes.multimethod - def joinpath(cls: Self, first: str, *others: str) -> Self: ... - def splitall(self: Self) -> List[Union[Self, str]]: ... - def parts(self: Self) -> Tuple[Union[Self, str], ...]: ... - def _parts(self: Self) -> Iterator[Union[Self, str]]: ... - def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: ... - def relpath(self: Self, start: str = ...) -> Self: ... - def relpathto(self: Self, dest: str) -> Self: ... + def joinpath(cls: Self, first: str, *others: str) -> Self: + ... + + def splitall(self: Self) -> List[Union[Self, str]]: + ... + + def parts(self: Self) -> Tuple[Union[Self, str], ...]: + ... + + def _parts(self: Self) -> Iterator[Union[Self, str]]: + ... + + def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: + ... + + def relpath(self: Self, start: str = ...) -> Self: + ... + + def relpathto(self: Self, dest: str) -> Self: + ... + # --- Listing, searching, walking, and matching - def listdir(self: Self, match: _Match = ...) -> List[Self]: ... - def dirs(self: Self, match: _Match = ...) -> List[Self]: ... - def files(self: Self, match: _Match = ...) -> List[Self]: ... + def listdir(self: Self, match: _Match = ...) -> List[Self]: + ... + + def dirs(self: Self, match: _Match = ...) -> List[Self]: + ... + + def files(self: Self, match: _Match = ...) -> List[Self]: + ... + def walk( self: Self, match: _Match = ..., errors: str = ..., - ) -> Generator[Self, Optional[Callable[[], bool]], None]: ... + ) -> Generator[Self, Optional[Callable[[], bool]], None]: + ... + def walkdirs( self: Self, match: _Match = ..., errors: str = ..., - ) -> Iterator[Self]: ... + ) -> Iterator[Self]: + ... + def walkfiles( self: Self, match: _Match = ..., errors: str = ..., - ) -> Iterator[Self]: ... + ) -> Iterator[Self]: + ... + def fnmatch( self, pattern: Union[Path, str], normcase: Optional[Callable[[str], str]] = ..., - ) -> bool: ... - def glob(self: Self, pattern: str) -> List[Self]: ... - def iglob(self: Self, pattern: str) -> Iterator[Self]: ... + ) -> bool: + ... + + def glob(self: Self, pattern: str) -> List[Self]: + ... + + def iglob(self: Self, pattern: str) -> Iterator[Self]: + ... + @overload def open( self, @@ -148,7 +252,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> TextIOWrapper: ... + ) -> TextIOWrapper: + ... + @overload def open( self, @@ -159,7 +265,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> FileIO: ... + ) -> FileIO: + ... + @overload def open( self, @@ -170,7 +278,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedRandom: ... + ) -> BufferedRandom: + ... + @overload def open( self, @@ -181,7 +291,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedReader: ... + ) -> BufferedReader: + ... + @overload def open( self, @@ -192,7 +304,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedWriter: ... + ) -> BufferedWriter: + ... + @overload def open( self, @@ -203,7 +317,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BinaryIO: ... + ) -> BinaryIO: + ... + @overload def open( self, @@ -214,8 +330,12 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> IO[Any]: ... - def bytes(self) -> builtins.bytes: ... + ) -> IO[Any]: + ... + + def bytes(self) -> builtins.bytes: + ... + @overload def chunks( self, @@ -227,7 +347,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[str]: ... + ) -> Iterator[str]: + ... + @overload def chunks( self, @@ -239,7 +361,9 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[builtins.bytes]: ... + ) -> Iterator[builtins.bytes]: + ... + @overload def chunks( self, @@ -251,13 +375,23 @@ class Path(str): newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[Union[str, builtins.bytes]]: ... - def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... + ) -> Iterator[Union[str, builtins.bytes]]: + ... + + def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: + ... + def read_text( self, encoding: Optional[str] = ..., errors: Optional[str] = ... - ) -> str: ... - def read_bytes(self) -> builtins.bytes: ... - def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: ... + ) -> str: + ... + + def read_bytes(self) -> builtins.bytes: + ... + + def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: + ... + @overload def write_text( self, @@ -266,7 +400,9 @@ class Path(str): errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: ... + ) -> None: + ... + @overload def write_text( self, @@ -275,13 +411,17 @@ class Path(str): errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: ... + ) -> None: + ... + def lines( self, encoding: Optional[str] = ..., errors: Optional[str] = ..., retain: bool = ..., - ) -> List[str]: ... + ) -> List[str]: + ... + def write_lines( self, lines: List[str], @@ -289,29 +429,67 @@ class Path(str): errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: ... - def read_md5(self) -> builtins.bytes: ... - def read_hash(self, hash_name: str) -> builtins.bytes: ... - def read_hexhash(self, hash_name: str) -> str: ... - def isabs(self) -> bool: ... - def exists(self) -> bool: ... - def isdir(self) -> bool: ... - def isfile(self) -> bool: ... - def islink(self) -> bool: ... - def ismount(self) -> bool: ... - def samefile(self, other: str) -> bool: ... - def getatime(self) -> float: ... + ) -> None: + ... + + def read_md5(self) -> builtins.bytes: + ... + + def read_hash(self, hash_name: str) -> builtins.bytes: + ... + + def read_hexhash(self, hash_name: str) -> str: + ... + + def isabs(self) -> bool: + ... + + def exists(self) -> bool: + ... + + def isdir(self) -> bool: + ... + + def isfile(self) -> bool: + ... + + def islink(self) -> bool: + ... + + def ismount(self) -> bool: + ... + + def samefile(self, other: str) -> bool: + ... + + def getatime(self) -> float: + ... + @property - def atime(self) -> float: ... - def getmtime(self) -> float: ... + def atime(self) -> float: + ... + + def getmtime(self) -> float: + ... + @property - def mtime(self) -> float: ... - def getctime(self) -> float: ... + def mtime(self) -> float: + ... + + def getctime(self) -> float: + ... + @property - def ctime(self) -> float: ... - def getsize(self) -> int: ... + def ctime(self) -> float: + ... + + def getsize(self) -> int: + ... + @property - def size(self) -> int: ... + def size(self) -> int: + ... + def access( self, mode: int, @@ -319,15 +497,29 @@ class Path(str): dir_fd: Optional[int] = ..., effective_ids: bool = ..., follow_symlinks: bool = ..., - ) -> bool: ... - def stat(self) -> os.stat_result: ... - def lstat(self) -> os.stat_result: ... - def get_owner(self) -> str: ... + ) -> bool: + ... + + def stat(self) -> os.stat_result: + ... + + def lstat(self) -> os.stat_result: + ... + + def get_owner(self) -> str: + ... + @property - def owner(self) -> str: ... + def owner(self) -> str: + ... + if sys.platform != 'win32': - def statvfs(self) -> os.statvfs_result: ... - def pathconf(self, name: Union[str, int]) -> int: ... + + def statvfs(self) -> os.statvfs_result: + ... + + def pathconf(self, name: Union[str, int]) -> int: + ... def utime( self, @@ -336,37 +528,91 @@ class Path(str): ns: Tuple[int, int] = ..., dir_fd: Optional[int] = ..., follow_symlinks: bool = ..., - ) -> Path: ... - def chmod(self: Self, mode: Union[str, int]) -> Self: ... + ) -> Path: + ... + + def chmod(self: Self, mode: Union[str, int]) -> Self: + ... + if sys.platform != 'win32': + def chown( self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ... - ) -> Self: ... - - def rename(self: Self, new: str) -> Self: ... - def renames(self: Self, new: str) -> Self: ... - def mkdir(self: Self, mode: int = ...) -> Self: ... - def mkdir_p(self: Self, mode: int = ...) -> Self: ... - def makedirs(self: Self, mode: int = ...) -> Self: ... - def makedirs_p(self: Self, mode: int = ...) -> Self: ... - def rmdir(self: Self) -> Self: ... - def rmdir_p(self: Self) -> Self: ... - def removedirs(self: Self) -> Self: ... - def removedirs_p(self: Self) -> Self: ... - def touch(self: Self) -> Self: ... - def remove(self: Self) -> Self: ... - def remove_p(self: Self) -> Self: ... - def unlink(self: Self) -> Self: ... - def unlink_p(self: Self) -> Self: ... - def link(self: Self, newpath: str) -> Self: ... - def symlink(self: Self, newlink: Optional[str] = ...) -> Self: ... - def readlink(self: Self) -> Self: ... - def readlinkabs(self: Self) -> Self: ... - def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... - def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copy(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... - def copy2(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... + ) -> Self: + ... + + def rename(self: Self, new: str) -> Self: + ... + + def renames(self: Self, new: str) -> Self: + ... + + def mkdir(self: Self, mode: int = ...) -> Self: + ... + + def mkdir_p(self: Self, mode: int = ...) -> Self: + ... + + def makedirs(self: Self, mode: int = ...) -> Self: + ... + + def makedirs_p(self: Self, mode: int = ...) -> Self: + ... + + def rmdir(self: Self) -> Self: + ... + + def rmdir_p(self: Self) -> Self: + ... + + def removedirs(self: Self) -> Self: + ... + + def removedirs_p(self: Self) -> Self: + ... + + def touch(self: Self) -> Self: + ... + + def remove(self: Self) -> Self: + ... + + def remove_p(self: Self) -> Self: + ... + + def unlink(self: Self) -> Self: + ... + + def unlink_p(self: Self) -> Self: + ... + + def link(self: Self, newpath: str) -> Self: + ... + + def symlink(self: Self, newlink: Optional[str] = ...) -> Self: + ... + + def readlink(self: Self) -> Self: + ... + + def readlinkabs(self: Self) -> Self: + ... + + def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: + ... + + def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: + ... + + def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: + ... + + def copy(self, dst: str, *, follow_symlinks: bool = ...) -> Any: + ... + + def copy2(self, dst: str, *, follow_symlinks: bool = ...) -> Any: + ... + def copytree( self, dst: str, @@ -375,18 +621,28 @@ class Path(str): copy_function: Callable[[str, str], None] = ..., ignore_dangling_symlinks: bool = ..., dirs_exist_ok: bool = ..., - ) -> Any: ... - def move( - self, dst: str, copy_function: Callable[[str, str], None] = ... - ) -> Any: ... + ) -> Any: + ... + + def move(self, dst: str, copy_function: Callable[[str, str], None] = ...) -> Any: + ... + def rmtree( self, ignore_errors: bool = ..., onerror: Optional[Callable[[Any, Any, Any], Any]] = ..., - ) -> None: ... - def rmtree_p(self: Self) -> Self: ... - def chdir(self) -> None: ... - def cd(self) -> None: ... + ) -> None: + ... + + def rmtree_p(self: Self) -> Self: + ... + + def chdir(self) -> None: + ... + + def cd(self) -> None: + ... + def merge_tree( self, dst: str, @@ -394,11 +650,18 @@ class Path(str): *, copy_function: Callable[[str, str], None] = ..., ignore: Callable[[Any, List[str]], Union[List[str], Set[str]]] = ..., - ) -> None: ... + ) -> None: + ... + if sys.platform != 'win32': - def chroot(self) -> None: ... + + def chroot(self) -> None: + ... + if sys.platform == 'win32': - def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... + + def startfile(self: Self, operation: Optional[str] = ...) -> Self: + ... @contextlib.contextmanager def in_place( @@ -409,27 +672,42 @@ class Path(str): errors: Optional[str] = ..., newline: Optional[str] = ..., backup_extension: Optional[str] = ..., - ) -> Iterator[Tuple[IO[Any], IO[Any]]]: ... + ) -> Iterator[Tuple[IO[Any], IO[Any]]]: + ... + @classes.ClassProperty @classmethod - def special(cls) -> Callable[[Optional[str]], SpecialResolver]: ... + def special(cls) -> Callable[[Optional[str]], SpecialResolver]: + ... + class DirectoryNotEmpty(OSError): @staticmethod - def translate() -> Iterator[None]: ... + def translate() -> Iterator[None]: + ... + + +def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: + ... -def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: ... class ExtantPath(Path): - def _validate(self) -> None: ... + def _validate(self) -> None: + ... + class ExtantFile(Path): - def _validate(self) -> None: ... + def _validate(self) -> None: + ... + class SpecialResolver: class ResolverScope: - def __init__(self, paths: SpecialResolver, scope: str) -> None: ... - def __getattr__(self, class_: str) -> MultiPathType: ... + def __init__(self, paths: SpecialResolver, scope: str) -> None: + ... + + def __getattr__(self, class_: str) -> MultiPathType: + ... def __init__( self, @@ -439,44 +717,70 @@ class SpecialResolver: version: Optional[str] = ..., roaming: bool = ..., multipath: bool = ..., - ): ... - def __getattr__(self, scope: str) -> ResolverScope: ... - def get_dir(self, scope: str, class_: str) -> MultiPathType: ... + ): + ... + + def __getattr__(self, scope: str) -> ResolverScope: + ... + + def get_dir(self, scope: str, class_: str) -> MultiPathType: + ... + class Multi: @classmethod - def for_class(cls, path_cls: type) -> Type[MultiPathType]: ... + def for_class(cls, path_cls: type) -> Type[MultiPathType]: + ... + @classmethod - def detect(cls, input: str) -> MultiPathType: ... - def __iter__(self) -> Iterator[Path]: ... + def detect(cls, input: str) -> MultiPathType: + ... + + def __iter__(self) -> Iterator[Path]: + ... + @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: ... + def _next_class(cls) -> Type[Path]: + ... + class MultiPathType(Multi, Path): pass + class TempDir(Path): @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: ... + def _next_class(cls) -> Type[Path]: + ... + def __new__( cls: Type[Self], suffix: Optional[AnyStr] = ..., prefix: Optional[AnyStr] = ..., dir: Optional[Union[AnyStr, os.PathLike[AnyStr]]] = ..., - ) -> Self: ... - def __init__(self) -> None: ... - def __enter__(self) -> Path: ... # type: ignore + ) -> Self: + ... + + def __init__(self) -> None: + ... + + def __enter__(self) -> Path: + ... # type: ignore + def __exit__( self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: ... + ) -> None: + ... + class Handlers: @classmethod def _resolve( cls, param: Union[str, Callable[[str], None]] - ) -> Callable[[str], None]: ... + ) -> Callable[[str], None]: + ... diff --git a/path/classes.pyi b/path/classes.pyi index 2878c48b..26cc2c85 100644 --- a/path/classes.pyi +++ b/path/classes.pyi @@ -1,8 +1,14 @@ from typing import Any, Callable, Optional + class ClassProperty(property): - def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: ... + def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: + ... + class multimethod: - def __init__(self, func: Callable[..., Any]): ... - def __get__(self, instance: Any, owner: Optional[type]) -> Any: ... + def __init__(self, func: Callable[..., Any]): + ... + + def __get__(self, instance: Any, owner: Optional[type]) -> Any: + ... diff --git a/path/matchers.pyi b/path/matchers.pyi index 80acd0b1..f045f2b8 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -6,23 +6,41 @@ from typing_extensions import Literal from path import Path + @overload -def load(param: None) -> Null: ... +def load(param: None) -> Null: + ... + + @overload -def load(param: str) -> Pattern: ... +def load(param: str) -> Pattern: + ... + + @overload -def load(param: Any) -> Any: ... +def load(param: Any) -> Any: + ... + class Base: pass + class Null(Base): - def __call__(self, path: str) -> Literal[True]: ... + def __call__(self, path: str) -> Literal[True]: + ... + class Pattern(Base): - def __init__(self, pattern: str) -> None: ... - def get_pattern(self, normcase: Callable[[str], str]) -> str: ... - def __call__(self, path: Path) -> bool: ... + def __init__(self, pattern: str) -> None: + ... + + def get_pattern(self, normcase: Callable[[str], str]) -> str: + ... + + def __call__(self, path: Path) -> bool: + ... + class CaseInsensitive(Pattern): normcase: Callable[[str], str] From 6fb137a25b7957576825425cb1c832711037c156 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 21:37:11 -0400 Subject: [PATCH 640/835] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index 09df8e7c..7f828cbc 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -766,8 +766,8 @@ class TempDir(Path): def __init__(self) -> None: ... - def __enter__(self) -> Path: - ... # type: ignore + def __enter__(self) -> Path: # type: ignore + ... def __exit__( self, From 1465f6a1c7d205114f02fa0f6244fcd6b8d9a265 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 21:41:26 -0400 Subject: [PATCH 641/835] Move 'overload' types inline. --- path/__init__.py | 186 ++++++++++++++++++++++++++++++++++++++++++++++ path/__init__.pyi | 170 ------------------------------------------ path/matchers.py | 20 +++++ path/matchers.pyi | 18 +---- 4 files changed, 207 insertions(+), 187 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 7d46d1d2..827eebd0 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -21,7 +21,9 @@ # Concatenate paths with / foo_txt = Path("bar") / "foo.txt" """ +from __future__ import annotations +import builtins import sys import warnings import os @@ -49,6 +51,35 @@ with contextlib.suppress(ImportError): import grp +from io import ( + BufferedRandom, + BufferedReader, + BufferedWriter, + FileIO, + TextIOWrapper, +) +from typing import ( + Any, + BinaryIO, + Callable, + IO, + Iterator, + Optional, + overload, +) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from _typeshed import ( + OpenBinaryMode, + OpenBinaryModeUpdating, + OpenBinaryModeReading, + OpenBinaryModeWriting, + OpenTextMode, + ) + from typing_extensions import Literal + from . import matchers from . import masks from . import classes @@ -615,6 +646,97 @@ def iglob(self, pattern): # # --- Reading or writing an entire file at once. + @overload + def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> TextIOWrapper: + ... + + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> FileIO: + ... + + @overload + def open( + self, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedRandom: + ... + + @overload + def open( + self, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedReader: + ... + + @overload + def open( + self, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BufferedWriter: + ... + + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: int, + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> BinaryIO: + ... + + @overload + def open( + self, + mode: str, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] = ..., + ) -> IO[Any]: + ... + def open(self, *args, **kwargs): """Open this file and return a corresponding file object. @@ -628,6 +750,48 @@ def bytes(self): with self.open('rb') as f: return f.read() + @overload + def chunks( + self, + size: int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[str]: + ... + + @overload + def chunks( + self, + size: int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[builtins.bytes]: + ... + + @overload + def chunks( + self, + size: int, + mode: str, + buffering: int = ..., + encoding: Optional[str] = ..., + errors: Optional[str] = ..., + newline: Optional[str] = ..., + closefd: bool = ..., + opener: Optional[Callable[[str, int], int]] = ..., + ) -> Iterator[Union[str, builtins.bytes]]: + ... + def chunks(self, size, *args, **kwargs): """Returns a generator yielding chunks of the file, so it can be read piece by piece with a simple for loop. @@ -681,6 +845,28 @@ def text(self, encoding=None, errors='strict'): ) return U_NEWLINE.sub('\n', self.read_text(encoding, errors)) + @overload + def write_text( + self, + text: str, + encoding: Optional[str] = ..., + errors: str = ..., + linesep: Optional[str] = ..., + append: bool = ..., + ) -> None: + ... + + @overload + def write_text( + self, + text: builtins.bytes, + encoding: None = ..., + errors: str = ..., + linesep: Optional[str] = ..., + append: bool = ..., + ) -> None: + ... + def write_text( self, text, encoding=None, errors='strict', linesep=os.linesep, append=False ): diff --git a/path/__init__.pyi b/path/__init__.pyi index 7f828cbc..d141d52d 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -4,18 +4,10 @@ import builtins import contextlib import os import sys -from io import ( - BufferedRandom, - BufferedReader, - BufferedWriter, - FileIO, - TextIOWrapper, -) from types import ModuleType, TracebackType from typing import ( Any, AnyStr, - BinaryIO, Callable, Generator, Iterable, @@ -27,18 +19,11 @@ from typing import ( Tuple, Type, Union, - overload, ) from _typeshed import ( - OpenBinaryMode, - OpenBinaryModeUpdating, - OpenBinaryModeReading, - OpenBinaryModeWriting, - OpenTextMode, Self, ) -from typing_extensions import Literal from . import classes @@ -242,142 +227,9 @@ class Path(str): def iglob(self: Self, pattern: str) -> Iterator[Self]: ... - @overload - def open( - self, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., - ) -> TextIOWrapper: - ... - - @overload - def open( - self, - mode: OpenBinaryMode, - buffering: Literal[0], - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> FileIO: - ... - - @overload - def open( - self, - mode: OpenBinaryModeUpdating, - buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedRandom: - ... - - @overload - def open( - self, - mode: OpenBinaryModeReading, - buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedReader: - ... - - @overload - def open( - self, - mode: OpenBinaryModeWriting, - buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedWriter: - ... - - @overload - def open( - self, - mode: OpenBinaryMode, - buffering: int, - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BinaryIO: - ... - - @overload - def open( - self, - mode: str, - buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> IO[Any]: - ... - def bytes(self) -> builtins.bytes: ... - @overload - def chunks( - self, - size: int, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[str]: - ... - - @overload - def chunks( - self, - size: int, - mode: OpenBinaryMode, - buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[builtins.bytes]: - ... - - @overload - def chunks( - self, - size: int, - mode: str, - buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[Union[str, builtins.bytes]]: - ... - def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... @@ -392,28 +244,6 @@ class Path(str): def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: ... - @overload - def write_text( - self, - text: str, - encoding: Optional[str] = ..., - errors: str = ..., - linesep: Optional[str] = ..., - append: bool = ..., - ) -> None: - ... - - @overload - def write_text( - self, - text: builtins.bytes, - encoding: None = ..., - errors: str = ..., - linesep: Optional[str] = ..., - append: bool = ..., - ) -> None: - ... - def lines( self, encoding: Optional[str] = ..., diff --git a/path/matchers.py b/path/matchers.py index 63ca218a..cb0df10d 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -1,7 +1,27 @@ +from __future__ import annotations + import ntpath import fnmatch +from typing import Any, overload + + +@overload +def load(param: None) -> Null: + ... + + +@overload +def load(param: str) -> Pattern: + ... + + +@overload +def load(param: Any) -> Any: + ... + + def load(param): """ If the supplied parameter is a string, assume it's a simple diff --git a/path/matchers.pyi b/path/matchers.pyi index f045f2b8..2d28710d 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -1,27 +1,11 @@ from __future__ import annotations -from typing import Any, Callable, overload +from typing import Callable from typing_extensions import Literal from path import Path - -@overload -def load(param: None) -> Null: - ... - - -@overload -def load(param: str) -> Pattern: - ... - - -@overload -def load(param: Any) -> Any: - ... - - class Base: pass From 69d599873d8a8d8a660bac3894872e3017f96bbe Mon Sep 17 00:00:00 2001 From: 5j9 <5j9@users.noreply.github.com> Date: Fri, 7 Jul 2023 05:31:27 +0330 Subject: [PATCH 642/835] Use '.' as the default value for Path constructor closes #212 --- path/__init__.py | 4 +++- test_path.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 7d46d1d2..c8ef6a4b 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -141,8 +141,10 @@ class Path(str): .. seealso:: :mod:`os.path` """ + def __new__(cls, /, object='.'): + return super().__new__(cls, object) - def __init__(self, other=''): + def __init__(self, other='.'): if other is None: raise TypeError("Invalid initial value for path: None") with contextlib.suppress(AttributeError): diff --git a/test_path.py b/test_path.py index d1053692..0bd3ffd1 100644 --- a/test_path.py +++ b/test_path.py @@ -78,6 +78,12 @@ def test_relpath(self): d = Path('D:\\') assert d.relpathto(boz) == boz + def test_construction_without_args(self): + """ + Path class will construct a path to current directory when called with no arguments. + """ + assert Path() == '.' + def test_construction_from_none(self): """ """ with pytest.raises(TypeError): @@ -432,7 +438,7 @@ def test_startfile(monkeypatch): results = [] monkeypatch.setattr(os, 'startfile', results.append) Path().startfile() - assert results == [''] + assert results == ['.'] class TestScratchDir: From 69346dd1cc0ea32999d643d4df7df8a6ac66df99 Mon Sep 17 00:00:00 2001 From: 5j9 <5j9@users.noreply.github.com> Date: Fri, 7 Jul 2023 05:50:02 +0330 Subject: [PATCH 643/835] Use `other` instead of `object` (a built-in name) --- path/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index c8ef6a4b..c0c15ac8 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -141,8 +141,8 @@ class Path(str): .. seealso:: :mod:`os.path` """ - def __new__(cls, /, object='.'): - return super().__new__(cls, object) + def __new__(cls, /, other='.'): + return super().__new__(cls, other) def __init__(self, other='.'): if other is None: From 79e9c94026a0667da4507962f99cd43ac47f675e Mon Sep 17 00:00:00 2001 From: 5j9 <5j9@users.noreply.github.com> Date: Fri, 7 Jul 2023 19:17:51 +0330 Subject: [PATCH 644/835] Remove unnecessary usage of positional-only syntax --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index c0c15ac8..bd35acf5 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -141,7 +141,7 @@ class Path(str): .. seealso:: :mod:`os.path` """ - def __new__(cls, /, other='.'): + def __new__(cls, other='.'): return super().__new__(cls, other) def __init__(self, other='.'): From a7310562fad7a8834c9810c1edd8e00b03e1394b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 21:48:23 -0400 Subject: [PATCH 645/835] Increase visibility of security policy. (#4) * Create SECURITY.md Signed-off-by: Joyce * Remove the security contact from the README, as it's now redundant. Closes jaraco/tidelift#3. --------- Signed-off-by: Joyce Co-authored-by: Joyce --- README.rst | 7 ------- SECURITY.md | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) create mode 100644 SECURITY.md diff --git a/README.rst b/README.rst index 7b317c71..087365cd 100644 --- a/README.rst +++ b/README.rst @@ -9,10 +9,3 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..54f99acb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Contact + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. From c43962adf34c28c22573093419e5e98b2e57cc07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jul 2023 23:34:53 -0400 Subject: [PATCH 646/835] Remove TOX_WORK_DIR workaround, no longer necessary with tox 4. Ref tox-dev/tox#3050. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 1093e028..e51d652d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,3 @@ -[tox] -toxworkdir={env:TOX_WORK_DIR:.tox} - - [testenv] deps = setenv = From 0e2032c4754c598ba75e467c64009ba4490ddea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Aug 2023 18:42:14 -0400 Subject: [PATCH 647/835] Pin against sphinx 7.2.5 as workaround for sphinx/sphinx-doc#11662. Closes jaraco/skeleton#88. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 46f7bdf7..4f184c7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,8 @@ testing = docs = # upstream sphinx >= 3.5 + # workaround for sphinx/sphinx-doc#11662 + sphinx < 7.2.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo From 92d2d8e1aff997f3877239230c9490ed9cdd1222 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:46:27 -0400 Subject: [PATCH 648/835] Allow GITHUB_* settings to pass through to tests. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8224099..67d9d3bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,10 @@ env: # Must be "1". TOX_PARALLEL_NO_SPINNER: 1 + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_* + jobs: test: From f3dc1f4776c94a9a4a7c0e8c5b49c532b0a7d411 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:49:13 -0400 Subject: [PATCH 649/835] Remove spinner disablement. If it's not already fixed upstream, that's where it should be fixed. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 67d9d3bc..30c9615d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,10 +32,6 @@ env: PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' - # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream - # Must be "1". - TOX_PARALLEL_NO_SPINNER: 1 - # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- testenv.pass_env+=GITHUB_* From 0484daa8a6f72c9ad4e1784f9181c2488a191d8e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:53:55 -0400 Subject: [PATCH 650/835] Clean up 'color' environment variables. The TOX_TESTENV_PASSENV hasn't been useful for some time and by its mere presence wasted a lot of time today under the assumption that it's doing something. Instead, just rely on one variable FORCE_COLOR. If it's not honored, then that should be the fix upstream. --- .github/workflows/main.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30c9615d..f3028549 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,26 +6,8 @@ permissions: contents: read env: - # Environment variables to support color support (jaraco/skeleton#66): - # Request colored output from CLI tools supporting it. Different tools - # interpret the value differently. For some, just being set is sufficient. - # For others, it must be a non-zero integer. For yet others, being set - # to a non-empty value is sufficient. For tox, it must be one of - # , 0, 1, false, no, off, on, true, yes. The only enabling value - # in common is "1". + # Environment variable to support color support (jaraco/skeleton#66) FORCE_COLOR: 1 - # MyPy's color enforcement (must be a non-zero number) - MYPY_FORCE_COLOR: -42 - # Recognized by the `py` package, dependency of `pytest` (must be "1") - PY_COLORS: 1 - # Make tox-wrapped tools see color requests - TOX_TESTENV_PASSENV: >- - FORCE_COLOR - MYPY_FORCE_COLOR - NO_COLOR - PY_COLORS - PYTEST_THEME - PYTEST_THEME_MODE # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' From b02bf32bae729d53bdb7c9649d6ec36afdb793ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:27:03 -0400 Subject: [PATCH 651/835] Add diff-cover check to Github Actions CI. Closes jaraco/skeleton#90. --- .github/workflows/main.yml | 18 ++++++++++++++++++ tox.ini | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3028549..fa326a26 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,6 +53,24 @@ jobs: - name: Run run: tox + diffcov: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Evaluate coverage + run: tox + env: + TOXENV: diffcov + docs: runs-on: ubuntu-latest env: diff --git a/tox.ini b/tox.ini index e51d652d..3b4414b4 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,14 @@ usedevelop = True extras = testing +[testenv:diffcov] +deps = + diff-cover +commands = + pytest {posargs} --cov-report xml + diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 + [testenv:docs] extras = docs From a6256e2935468b72a61aa7fda1e036faef3bfb3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:59:47 -0400 Subject: [PATCH 652/835] Add descriptions to the tox environments. Closes jaraco/skeleton#91. --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 3b4414b4..1950b4ef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [testenv] +description = perform primary checks (tests, style, types, coverage) deps = setenv = PYTHONWARNDEFAULTENCODING = 1 @@ -9,6 +10,7 @@ extras = testing [testenv:diffcov] +description = run tests and check that diff from main is covered deps = diff-cover commands = @@ -17,6 +19,7 @@ commands = diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] +description = build the documentation extras = docs testing @@ -26,6 +29,7 @@ commands = python -m sphinxlint [testenv:finalize] +description = assemble changelog and tag a release skip_install = True deps = towncrier @@ -36,6 +40,7 @@ commands = [testenv:release] +description = publish the package to PyPI and GitHub skip_install = True deps = build From 928e9a86d61d3a660948bcba7689f90216cc8243 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 14:10:31 -0400 Subject: [PATCH 653/835] Add FORCE_COLOR to the TOX_OVERRIDE for GHA. Requires tox 4.11.1. Closes jaraco/skeleton#89. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa326a26..28e36786 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ env: # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- - testenv.pass_env+=GITHUB_* + testenv.pass_env+=GITHUB_*,FORCE_COLOR jobs: From ca1831c2148fe5ddbffd001de76ff5f6005f812c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2023 11:05:36 -0400 Subject: [PATCH 654/835] Prefer ``pass_env`` in tox config. Preferred failure mode for tox-dev/tox#3127 and closes jaraco/skeleton#92. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1950b4ef..33da3deb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ skip_install = True deps = towncrier jaraco.develop >= 7.23 -passenv = * +pass_env = * commands = python -m jaraco.develop.finalize @@ -46,7 +46,7 @@ deps = build twine>=3 jaraco.develop>=7.1 -passenv = +pass_env = TWINE_PASSWORD GITHUB_TOKEN setenv = From 03f03e7802b0842b41f70b2b1c17ab26551a7533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:43:46 -0500 Subject: [PATCH 655/835] Limit sphinxlint jobs to 1. Workaround for sphinx-contrib/sphinx-lint#83. --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 33da3deb..331eeed9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,9 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint + python -m sphinxlint \ + # workaround for sphinx-contrib/sphinx-lint#83 + --jobs 1 [testenv:finalize] description = assemble changelog and tag a release From 75d9cc1b7cb6f84e7a16a83ec3abb9a478fdb130 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 19:57:45 +0600 Subject: [PATCH 656/835] Upgrade GitHub Actions checkout (jaraco/skeleton#94) Also, upgrade from `pypy3.9` to `pypy3.10` and remove the `continue-on-error` for Python 3.12. As recommended at jaraco/cssutils#41 --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28e36786..10828667 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,12 +36,12 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - - python: pypy3.9 + - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.12' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: @@ -56,7 +56,7 @@ jobs: diffcov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python @@ -76,7 +76,7 @@ jobs: env: TOXENV: docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 - name: Install tox @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: From 5732ebeeaa9480f8cd80c96a3183d7b247f27214 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 20:08:10 +0600 Subject: [PATCH 657/835] GitHub Actions: Combine tox jobs diffcov and docs (jaraco/skeleton#95) Code reuse Co-authored-by: Jason R. Coombs --- .github/workflows/main.yml | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10828667..9682985c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,12 +48,15 @@ jobs: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox - diffcov: + collateral: + strategy: + fail-fast: false + matrix: + job: [diffcov, docs] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -64,33 +67,16 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox - - name: Evaluate coverage - run: tox - env: - TOXENV: diffcov - - docs: - runs-on: ubuntu-latest - env: - TOXENV: docs - steps: - - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v4 - - name: Install tox - run: | - python -m pip install tox - - name: Run - run: tox + run: python -m pip install tox + - name: Eval ${{ matrix.job }} + run: tox -e ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() needs: - test - - docs + - collateral runs-on: ubuntu-latest @@ -115,8 +101,7 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox -e release env: From 26f420a97e73a2ab695023f6cc21f5c786d2b289 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Nov 2023 11:43:20 -0500 Subject: [PATCH 658/835] Remove news fragment after allowing time to be processed downstream. --- newsfragments/+drop-py37.feature.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. From 33dd01267b6a886217bae3ebd5df5b689e2ab722 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Nov 2023 13:21:17 -0500 Subject: [PATCH 659/835] Suppress deprecation warning in dateutil. Workaround for dateutil/dateutil#1284. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index d9a15ed1..f9533b57 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,4 +24,7 @@ filterwarnings= # pypa/build#615 ignore:'encoding' argument not specified::build.env + # dateutil/dateutil#1284 + ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz + ## end upstream From 54215092dd970c667c7234c6da5bfa0e3ad7ab89 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 13:57:37 -0500 Subject: [PATCH 660/835] Update README to reflect retirement of Python 3.5 and earlier. --- README.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index cf45a7fc..63baa187 100644 --- a/README.rst +++ b/README.rst @@ -63,6 +63,8 @@ based on ``path``. Advantages ========== +Path pie provides a superior experience to similar offerings. + Python 3.4 introduced `pathlib `_, which shares many characteristics with ``path``. In particular, @@ -75,9 +77,9 @@ has several advantages over ``pathlib``: - ``path`` implements ``Path`` objects as a subclass of ``str``, and as a result these ``Path`` objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one - must first cast values to strings before passing them to APIs unaware of - ``pathlib``. This shortcoming was somewhat `mitigated by PEP 519 - `_, in Python 3.6. + must first cast values to strings before passing them to APIs that do + not honor `PEP 519 `_ + ``PathLike`` interface. - ``path`` give quality of life features beyond exposing basic functionality of a path. ``path`` provides methods like ``rmtree`` (from shlib) and ``remove_p`` (remove a file if it exists), properties like ``.permissions``, @@ -85,16 +87,19 @@ has several advantages over ``pathlib``: - As a PyPI-hosted package, ``path`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. -- ``path`` provides a uniform abstraction over its Path object, +- ``path`` provides superior portability using a uniform abstraction + over its single Path object, freeing the implementer to subclass it readily. One cannot subclass a ``pathlib.Path`` to add functionality, but must subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even - if one only wishes to add a ``__dict__`` to the subclass + to do something as simple as to add a ``__dict__`` to the subclass instances. ``path`` instead allows the ``Path.module`` object to be overridden by subclasses, defaulting to the ``os.path``. Even advanced uses of ``path.Path`` that subclass the model do not need to be concerned with - OS-specific nuances. + OS-specific nuances. ``path.Path`` objects are inherently "pure", + not requiring the author to distinguish between pure and non-pure + variants. This path project has the explicit aim to provide compatibility with ``pathlib`` objects where possible, such that a ``path.Path`` From 3f33a116746d3f7ede82b6a8f5928640921d99df Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 14:50:02 -0500 Subject: [PATCH 661/835] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/path/__init__.py b/path/__init__.py index bd35acf5..2283175c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -141,6 +141,7 @@ class Path(str): .. seealso:: :mod:`os.path` """ + def __new__(cls, other='.'): return super().__new__(cls, other) From 7c6f16c4ffd0c2d7e595b0456369e9e94fad1675 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 14:50:42 -0500 Subject: [PATCH 662/835] Use the empty path in test_chroot and test_startfile. --- test_path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_path.py b/test_path.py index 0bd3ffd1..0bc0440d 100644 --- a/test_path.py +++ b/test_path.py @@ -430,7 +430,7 @@ def test_chroot(monkeypatch): results = [] monkeypatch.setattr(os, 'chroot', results.append) Path().chroot() - assert results == [''] + assert results == [Path()] @pytest.mark.skipif("not hasattr(Path, 'startfile')") @@ -438,7 +438,7 @@ def test_startfile(monkeypatch): results = [] monkeypatch.setattr(os, 'startfile', results.append) Path().startfile() - assert results == ['.'] + assert results == [Path()] class TestScratchDir: From 42c4778a7167e08915ab6e7dd6fa14b41a6039f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 14:53:46 -0500 Subject: [PATCH 663/835] Add news fragment. --- newsfragments/216.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/216.feature.rst diff --git a/newsfragments/216.feature.rst b/newsfragments/216.feature.rst new file mode 100644 index 00000000..ffa5f9df --- /dev/null +++ b/newsfragments/216.feature.rst @@ -0,0 +1 @@ +Use '.' as the default path. \ No newline at end of file From f1d623caff29bde754c5a0eca7a766d6959f7782 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 14:59:31 -0500 Subject: [PATCH 664/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/216.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/216.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 4efbedb5..ab878406 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.8.0 +======= + +Features +-------- + +- Use '.' as the default path. (#216) + + v16.7.1 ======= diff --git a/newsfragments/216.feature.rst b/newsfragments/216.feature.rst deleted file mode 100644 index ffa5f9df..00000000 --- a/newsfragments/216.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Use '.' as the default path. \ No newline at end of file From 8565be1a1faae95be4606a1b924b59a25bedb9c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:21:36 -0500 Subject: [PATCH 665/835] Added Path.iterdir() and deprecated Path.listdir(). Ref #214 --- newsfragments/214.feature.rst | 1 + path/__init__.py | 26 +++++++++------ path/matchers.py | 2 +- setup.cfg | 1 + test_path.py | 61 +++++++++++++++++++---------------- 5 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 newsfragments/214.feature.rst diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst new file mode 100644 index 00000000..904897f8 --- /dev/null +++ b/newsfragments/214.feature.rst @@ -0,0 +1 @@ +Added Path.iterdir() and deprecated Path.listdir(). Ref #214. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 2283175c..250a7e20 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -475,8 +475,8 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching - def listdir(self, match=None): - """List of items in this directory. + def iterdir(self, match=None): + """Yields items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing of just files or just subdirectories. @@ -489,7 +489,15 @@ def listdir(self, match=None): .. seealso:: :meth:`files`, :meth:`dirs` """ match = matchers.load(match) - return list(filter(match, (self / child for child in os.listdir(self)))) + return filter(match, (self / child for child in os.listdir(self))) + + def listdir(self, match=None): + warnings.warn( + ".listdir is deprecated; use iterdir", + DeprecationWarning, + stacklevel=2, + ) + return list(self.iterdir(match=match)) def dirs(self, *args, **kwargs): """List of this directory's subdirectories. @@ -498,9 +506,9 @@ def dirs(self, *args, **kwargs): This does not walk recursively into subdirectories (but see :meth:`walkdirs`). - Accepts parameters to :meth:`listdir`. + Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.listdir(*args, **kwargs) if p.isdir()] + return [p for p in self.iterdir(*args, **kwargs) if p.isdir()] def files(self, *args, **kwargs): """List of the files in self. @@ -508,10 +516,10 @@ def files(self, *args, **kwargs): The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). - Accepts parameters to :meth:`listdir`. + Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.listdir(*args, **kwargs) if p.isfile()] + return [p for p in self.iterdir(*args, **kwargs) if p.isfile()] def walk(self, match=None, errors='strict'): """Iterator over files and subdirs, recursively. @@ -534,7 +542,7 @@ def walk(self, match=None, errors='strict'): match = matchers.load(match) try: - childList = self.listdir() + childList = self.iterdir() except Exception as exc: errors(f"Unable to list directory '{self}': {exc}") return @@ -1336,7 +1344,7 @@ def merge_tree( dst = self._next_class(dst) dst.makedirs_p() - sources = self.listdir() + sources = list(self.iterdir()) _ignored = ignore(self, [item.name for item in sources]) def ignored(item): diff --git a/path/matchers.py b/path/matchers.py index 63ca218a..79636d65 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -46,7 +46,7 @@ def __call__(self, path): class CaseInsensitive(Pattern): """ A Pattern with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, + :meth:`iterdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. For example, to get all files ending in .py, .Py, .pY, or .PY in the diff --git a/setup.cfg b/setup.cfg index 722ffe7d..c21a3772 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ testing = appdirs packaging pywin32; platform_system == "Windows" and python_version < "3.12" + more_itertools # required for checkdocs on README.rst pygments diff --git a/test_path.py b/test_path.py index 0bc0440d..34804de1 100644 --- a/test_path.py +++ b/test_path.py @@ -30,6 +30,7 @@ import stat import pytest +from more_itertools import ilen import path from path import Path @@ -512,7 +513,7 @@ def test_touch(self, tmpdir): def test_listing(self, tmpdir): d = Path(tmpdir) - assert d.listdir() == [] + assert list(d.iterdir()) == [] f = 'testfile.txt' af = d / f @@ -521,7 +522,7 @@ def test_listing(self, tmpdir): try: assert af.exists() - assert d.listdir() == [af] + assert list(d.iterdir()) == [af] # .glob() assert d.glob('testfile.txt') == [af] @@ -545,7 +546,7 @@ def test_listing(self, tmpdir): with open(f, 'w', encoding='utf-8') as fobj: fobj.write('some text\n') try: - files2 = d.listdir() + files2 = list(d.iterdir()) files.sort() files2.sort() assert files == files2 @@ -565,17 +566,17 @@ def bytes_filename(self, tmpdir): raise pytest.skip(f"Invalid encodings disallowed {exc}") return name - def test_listdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover + def test_iterdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover """ Some filesystems allow non-character sequences in path names. - ``.listdir`` should still function in this case. + ``.iterdir`` should still function in this case. See issue #61 for details. """ # first demonstrate that os.listdir works assert os.listdir(str(tmpdir).encode('ascii')) # now try with path - results = Path(tmpdir).listdir() + results = Path(tmpdir).iterdir() (res,) = results assert isinstance(res, Path) assert len(res.basename()) == len(bytes_filename) @@ -663,7 +664,7 @@ def test_shutil(self, tmpdir): testA.copytree(testC) assert testC.isdir() self.assertSetsEqual( - testC.listdir(), + testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], ) assert not testCopyOfLink.islink() @@ -676,7 +677,7 @@ def test_shutil(self, tmpdir): testA.copytree(testC, True) assert testC.isdir() self.assertSetsEqual( - testC.listdir(), + testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], ) if hasattr(os, 'symlink'): @@ -686,7 +687,7 @@ def test_shutil(self, tmpdir): # Clean up. testDir.rmtree() assert not testDir.exists() - self.assertList(d.listdir(), []) + self.assertList(d.iterdir(), []) def assertList(self, listing, expected): assert sorted(listing) == sorted(expected) @@ -702,7 +703,7 @@ def test_patterns(self, tmpdir): for name in names: (e / name).touch() - self.assertList(d.listdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) + self.assertList(d.iterdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) self.assertList(d.files('*.tmp'), [d / 'x.tmp']) self.assertList(d.dirs('*.tmp'), [d / 'xdir.tmp']) self.assertList( @@ -922,7 +923,7 @@ def test_with_nonexisting_dst_kwargs(self): self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() def test_with_nonexisting_dst_args(self): @@ -932,7 +933,7 @@ def test_with_nonexisting_dst_args(self): self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() def test_with_existing_dst(self): @@ -953,7 +954,7 @@ def test_with_existing_dst(self): self.subdir_b / self.test_link.name, self.subdir_b / test_new.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 @@ -965,7 +966,7 @@ def test_copytree_parameters(self): self.subdir_a.merge_tree(self.subdir_b, ignore=ignore) assert self.subdir_b.isdir() - assert self.subdir_b.listdir() == [self.subdir_b / self.test_file.name] + assert list(self.subdir_b.iterdir()) == [self.subdir_b / self.test_file.name] def test_only_newer(self): """ @@ -985,6 +986,10 @@ def test_nested(self): self.subdir_a.merge_tree(self.subdir_b) assert self.subdir_b.joinpath('subsub').isdir() + def test_listdir(self): + with pytest.deprecated_call(): + Path().listdir() + class TestChdir: def test_chdir_or_cd(self, tmpdir): @@ -1111,22 +1116,22 @@ def normcase(path): assert p.fnmatch('foobar', normcase=normcase) assert p.fnmatch('FOO[ABC]AR', normcase=normcase) - def test_listdir_simple(self): + def test_iterdir_simple(self): p = Path('.') - assert len(p.listdir()) == len(os.listdir('.')) + assert ilen(p.iterdir()) == len(os.listdir('.')) - def test_listdir_empty_pattern(self): + def test_iterdir_empty_pattern(self): p = Path('.') - assert p.listdir('') == [] + assert list(p.iterdir('')) == [] - def test_listdir_patterns(self, tmpdir): + def test_iterdir_patterns(self, tmpdir): p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir('s*') == [p / 'sub'] - assert len(p.listdir('*')) == 2 + assert list(p.iterdir('s*')) == [p / 'sub'] + assert ilen(p.iterdir('*')) == 2 - def test_listdir_custom_module(self, tmpdir): + def test_iterdir_custom_module(self, tmpdir): """ Listdir patterns should honor the case sensitivity of the path module used by that Path class. @@ -1135,14 +1140,14 @@ def test_listdir_custom_module(self, tmpdir): p = always_unix(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir('S*') == [] + assert list(p.iterdir('S*')) == [] always_win = Path.using_module(ntpath) p = always_win(tmpdir) - assert p.listdir('S*') == [p / 'sub'] - assert p.listdir('f*') == [p / 'File'] + assert list(p.iterdir('S*')) == [p / 'sub'] + assert list(p.iterdir('f*')) == [p / 'File'] - def test_listdir_case_insensitive(self, tmpdir): + def test_iterdir_case_insensitive(self, tmpdir): """ Listdir patterns should honor the case sensitivity of the path module used by that Path class. @@ -1150,8 +1155,8 @@ def test_listdir_case_insensitive(self, tmpdir): p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir(matchers.CaseInsensitive('S*')) == [p / 'sub'] - assert p.listdir(matchers.CaseInsensitive('f*')) == [p / 'File'] + assert list(p.iterdir(matchers.CaseInsensitive('S*'))) == [p / 'sub'] + assert list(p.iterdir(matchers.CaseInsensitive('f*'))) == [p / 'File'] assert p.files(matchers.CaseInsensitive('S*')) == [] assert p.dirs(matchers.CaseInsensitive('f*')) == [] From 188538156d6cc77cfcb8e18a40dbca1def87dc54 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:26:09 -0500 Subject: [PATCH 666/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/214.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/214.feature.rst diff --git a/NEWS.rst b/NEWS.rst index ab878406..702d3d60 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.9.0 +======= + +Features +-------- + +- Added Path.iterdir() and deprecated Path.listdir(). Ref #214. (#214) + + v16.8.0 ======= diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst deleted file mode 100644 index 904897f8..00000000 --- a/newsfragments/214.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added Path.iterdir() and deprecated Path.listdir(). Ref #214. \ No newline at end of file From 6a6db4d46ffb27b7b84be883bc2d0a427885e156 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:45:19 -0500 Subject: [PATCH 667/835] Clean up NEWS. --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 702d3d60..c4630123 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -4,7 +4,7 @@ v16.9.0 Features -------- -- Added Path.iterdir() and deprecated Path.listdir(). Ref #214. (#214) +- Added ``.iterdir()`` and deprecated ``.listdir()``. (#214) v16.8.0 From 97a5f44787ac5a928534cdf724210c429621435c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:53:37 -0500 Subject: [PATCH 668/835] Update Github Actions badge per actions/starter-workflows#1525. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b703d490..41bcfbe8 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ .. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg -.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg +.. image:: https://github.com/PROJECT_PATH/actions/workflows/main.yml/badge.svg :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests From 8bff8b034a0bbf0273a38f0a0cc41e3a52b26864 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 5 Dec 2023 15:48:52 +0100 Subject: [PATCH 669/835] Enable testing merge queues @ GitHub Actions CI/CD (jaraco/skeleton#93) This allows org-hosted projects to start enabling merge queues in the repository settings. With that, GitHub would trigger a separate event against a merge commit derived from merging several pull requests with the target branch. --- .github/workflows/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9682985c..387d01aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,11 @@ name: tests -on: [push, pull_request] +on: + merge_group: + push: + branches-ignore: + - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + pull_request: permissions: contents: read From e4bd6091a1fbe26fe113051f0f47875d627c7ed2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Dec 2023 10:46:32 -0500 Subject: [PATCH 670/835] Separate collateral jobs on different lines for easier override/extension. --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 387d01aa..a079bbfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,9 @@ jobs: strategy: fail-fast: false matrix: - job: [diffcov, docs] + job: + - diffcov + - docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 596e6834c8a037c935338afe92e0b9c5ffa1768f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Dec 2023 18:29:16 -0500 Subject: [PATCH 671/835] Drop minimum requirement on pytest-mypy as most environments are already running much later. Closes jaraco/skeleton#96. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4f184c7e..20c5dd76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov - pytest-mypy >= 0.9.1; \ + pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 From b8c6c1530ef937521b60aabb0ecd98a8b5dca761 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:25:02 +0100 Subject: [PATCH 672/835] Use the ruff formatter (jaraco/skeleton#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use the ruff formatter, instead of black Based on: - ruff-pre-commit README.md | Using Ruff with pre-commit https://github.com/astral-sh/ruff-pre-commit/blob/main/README.md - The Ruff Formatter | Conflicting lint rules https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules Support for the ruff formatter was added to pytest-ruff by commits from October 2023, released the same day as versions 0.2 and 0.2.1. Hence, it makes sense to require pytest-ruff ≥ 0.2.1 now. Support for `quote-style = "preserve"` was added to ruff in the last couple of weeks, therefore require the latest version, ruff ≥ 0.1.8. This option is equivalent to `skip-string-normalization` in black. Closes jaraco/skeleton#101. --------- Co-authored-by: Jason R. Coombs --- .pre-commit-config.yaml | 7 ++++--- README.rst | 4 ---- pyproject.toml | 3 --- pytest.ini | 8 -------- ruff.toml | 22 ++++++++++++++++++++++ setup.cfg | 5 +---- 6 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af502010..5a4a7e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ repos: -- repo: https://github.com/psf/black - rev: 22.6.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 hooks: - - id: black + - id: ruff + - id: ruff-format diff --git a/README.rst b/README.rst index 41bcfbe8..2fabcf33 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,6 @@ :target: https://github.com/astral-sh/ruff :alt: Ruff -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest diff --git a/pyproject.toml b/pyproject.toml index dce944df..a853c578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,4 @@ requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" -[tool.black] -skip-string-normalization = true - [tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index f9533b57..022a723e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,14 +7,6 @@ filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning - # shopkeep/pytest-black#55 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning - ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - - # shopkeep/pytest-black#67 - ignore:'encoding' argument not specified::pytest_black - # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..7ed133b7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,22 @@ +[lint] +extend-ignore = [ + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] + +[format] +# https://docs.astral.sh/ruff/settings/#format-quote-style +quote-style = "preserve" diff --git a/setup.cfg b/setup.cfg index 20c5dd76..1d2729be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,15 +30,12 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-black >= 0.3.7; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" pytest-cov pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 - pytest-ruff + pytest-ruff >= 0.2.1 # local From a9c5dd5a4eab9f4132d62344cdbad24e077c650e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 Dec 2023 12:08:46 -0500 Subject: [PATCH 673/835] Remove sole entry for branches-ignore. Workaround for and closes jaraco/skeleton#103. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a079bbfb..cf94f7d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,8 @@ on: merge_group: push: branches-ignore: - - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # disabled for jaraco/skeleton#103 + # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches pull_request: permissions: From db0d581685d4fc2a16d392d4dedffe622e9a355c Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:58:23 +0100 Subject: [PATCH 674/835] =?UTF-8?q?ruff:=20extended-ignore=20=E2=86=92=20i?= =?UTF-8?q?gnore=20(jaraco/skeleton#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies Repo-Review suggestion: RF201: Avoid using deprecated config settings extend-ignore deprecated, use ignore instead (identical) --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 7ed133b7..795cca16 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -extend-ignore = [ +ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", From f6d9e107365ca270ec843898c05bb8e43dc6987a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Jan 2024 17:56:53 -0500 Subject: [PATCH 675/835] Bump year on badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2fabcf33..efabeee4 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2023-informational +.. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton From dbcb0747110d074112f27e2699856acfc4ba8ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:09:59 -0500 Subject: [PATCH 676/835] Remove build and dist from excludes. It appears they are not needed and their presence blocks the names of packages like 'builder' and 'distutils'. Ref pypa/distutils#224. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1d2729be..c2e82875 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,6 @@ install_requires = [options.packages.find] exclude = - build* - dist* docs* tests* From d27890573088a6a0292139c5e30466debd7dc1dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:26:16 -0500 Subject: [PATCH 677/835] Exclude docs and tests directories properly per Setuptools behavior. --- setup.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index c2e82875..c5aa1af9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,11 @@ install_requires = [options.packages.find] exclude = - docs* - tests* + # duplicate exclusions for pypa/setuptools#2688 + docs + docs.* + tests + tests.* [options.extras_require] testing = From 63535c6efd3516a7ef35c862c24ef5b6d43c8494 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:49:05 -0500 Subject: [PATCH 678/835] Rely on default discovery for good heuristics for finding packages. --- setup.cfg | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index c5aa1af9..fe99eaf6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,19 +13,10 @@ classifiers = Programming Language :: Python :: 3 :: Only [options] -packages = find_namespace: include_package_data = true python_requires = >=3.8 install_requires = -[options.packages.find] -exclude = - # duplicate exclusions for pypa/setuptools#2688 - docs - docs.* - tests - tests.* - [options.extras_require] testing = # upstream From b14a1c333569e879ad400e1829072a5148eb36a4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Feb 2024 22:32:24 -0500 Subject: [PATCH 679/835] Prefer .suffix to .ext and deprecate .ext. --- newsfragments/+50114b5a.feature.rst | 1 + path/__init__.py | 15 ++++++++++++--- test_path.py | 6 +++--- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 newsfragments/+50114b5a.feature.rst diff --git a/newsfragments/+50114b5a.feature.rst b/newsfragments/+50114b5a.feature.rst new file mode 100644 index 00000000..3fbfc68e --- /dev/null +++ b/newsfragments/+50114b5a.feature.rst @@ -0,0 +1 @@ +Prefer .suffix to .ext and deprecate .ext. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 250a7e20..2525af2d 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -278,10 +278,19 @@ def stem(self): return base @property - def ext(self): + def suffix(self): """The file extension, for example ``'.py'``.""" - f, ext = self.module.splitext(self) - return ext + f, suffix = self.module.splitext(self) + return suffix + + @property + def ext(self): + warnings.warn( + ".ext is deprecated; use suffix", + DeprecationWarning, + stacklevel=2, + ) + return self.suffix def with_suffix(self, suffix): """Return a new path with the file suffix changed (or added, if none) diff --git a/test_path.py b/test_path.py index 34804de1..8bc4dc9c 100644 --- a/test_path.py +++ b/test_path.py @@ -131,9 +131,9 @@ def test_properties(self): assert f.name == 'xyzzy.py' assert f.parent.name == os_choose(nt='Lib', posix='lib') - # .ext - assert f.ext == '.py' - assert f.parent.ext == '' + # .suffix + assert f.suffix == '.py' + assert f.parent.suffix == '' # .drive assert f.drive == os_choose(nt='C:', posix='') From b84c47dbe8eee8ec9fff12b30aa43af2193eb6c7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Feb 2024 22:38:58 -0500 Subject: [PATCH 680/835] Added .with_name and .with_stem. --- newsfragments/+ceb93420.feature.rst | 1 + path/__init__.py | 17 +++++++++++++++++ path/compat/py38.py | 13 +++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 newsfragments/+ceb93420.feature.rst create mode 100644 path/compat/py38.py diff --git a/newsfragments/+ceb93420.feature.rst b/newsfragments/+ceb93420.feature.rst new file mode 100644 index 00000000..ed853ad8 --- /dev/null +++ b/newsfragments/+ceb93420.feature.rst @@ -0,0 +1 @@ +Added .with_name and .with_stem. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 2525af2d..61e0237a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -52,6 +52,7 @@ from . import matchers from . import masks from . import classes +from .compat.py38 import removesuffix __all__ = ['Path', 'TempDir'] @@ -277,6 +278,14 @@ def stem(self): base, ext = self.module.splitext(self.name) return base + def with_stem(self, stem): + """Return a new path with the stem changed. + + >>> Path('/home/guido/python.tar.gz').with_stem("foo") + Path('/home/guido/foo.gz') + """ + return self.with_name(stem + self.suffix) + @property def suffix(self): """The file extension, for example ``'.py'``.""" @@ -347,6 +356,14 @@ def drive(self): """, ) + def with_name(self, name): + """Return a new path with the name changed. + + >>> Path('/home/guido/python.tar.gz').with_name("foo.zip") + Path('/home/guido/foo.zip') + """ + return self._next_class(removesuffix(self, self.name) + name) + def splitpath(self): """Return two-tuple of ``.parent``, ``.name``. diff --git a/path/compat/py38.py b/path/compat/py38.py new file mode 100644 index 00000000..84f45036 --- /dev/null +++ b/path/compat/py38.py @@ -0,0 +1,13 @@ +import sys + + +if sys.version_info < (3, 9): + def removesuffix(self, suffix): + # suffix='' should not call self[:-0]. + if suffix and self.endswith(suffix): + return self[:-len(suffix)] + else: + return self[:] +else: + def removesuffix(self, suffix): + return self.removesuffix(suffix) From 5f6532fe187c582cdff1afdeec1d926ef692a3ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Feb 2024 22:47:29 -0500 Subject: [PATCH 681/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+50114b5a.feature.rst | 1 - newsfragments/+ceb93420.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+50114b5a.feature.rst delete mode 100644 newsfragments/+ceb93420.feature.rst diff --git a/NEWS.rst b/NEWS.rst index c4630123..430681b1 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v16.10.0 +======== + +Features +-------- + +- Added .with_name and .with_stem. +- Prefer .suffix to .ext and deprecate .ext. + + v16.9.0 ======= diff --git a/newsfragments/+50114b5a.feature.rst b/newsfragments/+50114b5a.feature.rst deleted file mode 100644 index 3fbfc68e..00000000 --- a/newsfragments/+50114b5a.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Prefer .suffix to .ext and deprecate .ext. \ No newline at end of file diff --git a/newsfragments/+ceb93420.feature.rst b/newsfragments/+ceb93420.feature.rst deleted file mode 100644 index ed853ad8..00000000 --- a/newsfragments/+ceb93420.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added .with_name and .with_stem. \ No newline at end of file From 29e5d34af962e59e92c501ebb988dcaf192b114e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Feb 2024 10:15:04 -0500 Subject: [PATCH 682/835] Enable preview to enable preserving quotes. --- ruff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ruff.toml b/ruff.toml index 795cca16..e61ca8b0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -18,5 +18,7 @@ ignore = [ ] [format] +# Enable preview, required for quote-style = "preserve" +preview = true # https://docs.astral.sh/ruff/settings/#format-quote-style quote-style = "preserve" From dc9ce1b62c28604eafc4410bb65d336e3529a102 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Feb 2024 10:28:54 -0500 Subject: [PATCH 683/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/compat/py38.py | 14 ++++++++------ test_path.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/path/compat/py38.py b/path/compat/py38.py index 84f45036..a5f0fc1c 100644 --- a/path/compat/py38.py +++ b/path/compat/py38.py @@ -2,12 +2,14 @@ if sys.version_info < (3, 9): - def removesuffix(self, suffix): - # suffix='' should not call self[:-0]. - if suffix and self.endswith(suffix): - return self[:-len(suffix)] - else: - return self[:] + + def removesuffix(self, suffix): + # suffix='' should not call self[:-0]. + if suffix and self.endswith(suffix): + return self[: -len(suffix)] + else: + return self[:] else: + def removesuffix(self, suffix): return self.removesuffix(suffix) diff --git a/test_path.py b/test_path.py index 8bc4dc9c..6dbe143b 100644 --- a/test_path.py +++ b/test_path.py @@ -557,7 +557,7 @@ def test_listing(self, tmpdir): @pytest.fixture def bytes_filename(self, tmpdir): - name = br'r\xe9\xf1emi' + name = rb'r\xe9\xf1emi' base = str(tmpdir).encode('ascii') try: with open(os.path.join(base, name), 'wb'): From 2a402a39f154d9a6cf4621e8c5d22bace749b55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 6 Feb 2024 23:01:32 +0100 Subject: [PATCH 684/835] Tweak coverage configuration for type checking (jaraco/skeleton#97) * Tweak coverage configuration for type checking * Use `exclude_also` instead of `exclude_lines` Co-authored-by: Sviatoslav Sydorenko * Add reference to the issue. --------- Co-authored-by: Sviatoslav Sydorenko Co-authored-by: Jason R. Coombs --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 02879483..35b98b1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,3 +7,7 @@ disable_warnings = [report] show_missing = True +exclude_also = + # jaraco/skeleton#97 + @overload + if TYPE_CHECKING: From 68ac292eb37ce92e992e6fab05a44ad86f32e8f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Feb 2024 16:53:46 -0500 Subject: [PATCH 685/835] Use latest versions in RTD boilerplate. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 053c7287..68489063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,6 @@ python: # required boilerplate readthedocs/readthedocs.org#10401 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3" + python: latest From 178d254379ed260eb537f48722703f819eaa8235 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Feb 2024 16:02:29 -0500 Subject: [PATCH 686/835] Remove Sphinx pin. Ref sphinx-doc/sphinx#11662. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index fe99eaf6..400a72a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ testing = docs = # upstream sphinx >= 3.5 - # workaround for sphinx/sphinx-doc#11662 - sphinx < 7.2.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo From 779219ce3ecbf4477da062658a1d0b2d5bf4f77f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Feb 2024 10:38:06 -0500 Subject: [PATCH 687/835] Include deps from the base config in diffcov. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 331eeed9..4c39a5b1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ extras = [testenv:diffcov] description = run tests and check that diff from main is covered deps = + {[testenv]deps} diff-cover commands = pytest {posargs} --cov-report xml From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 688/835] Enable complexity check and pycodestyle warnings. Closes jaraco/skeleton#110. --- ruff.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruff.toml b/ruff.toml index e61ca8b0..6c5b0009 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,8 @@ [lint] +select = [ + "C901", + "W", +] ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", From b434f69238b4ee517ae20978afa19f3cd1ed8f1f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:05:46 -0500 Subject: [PATCH 689/835] Use 'extend-select' to avoid disabling the default config. Ref jaraco/skeleton#110. --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 6c5b0009..70612985 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -select = [ +extend-select = [ "C901", "W", ] From a0d0c4b7e87fbfd04cee2546ba452858587516fd Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 21 Mar 2024 15:34:23 -0400 Subject: [PATCH 690/835] Allow mypy on PyPy (jaraco/skeleton#111) https://github.com/pypa/setuptools/pull/4257 shows that mypy now works with PyPy --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 400a72a5..6fa73b6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,7 @@ testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-cov - pytest-mypy; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" + pytest-mypy pytest-enabler >= 2.2 pytest-ruff >= 0.2.1 From c9a7f97ba83be124e173713f5c24564c2b6dd49e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 15:49:52 -0400 Subject: [PATCH 691/835] Re-enable ignoring of temporary merge queue branches. Closes jaraco/skeleton#103. --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf94f7d8..143b0984 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,8 +4,11 @@ on: merge_group: push: branches-ignore: - # disabled for jaraco/skeleton#103 - # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # temporary GH branches relating to merge queues (jaraco/skeleton#93) + - gh-readonly-queue/** + tags: + # required if branches-ignore is supplied (jaraco/skeleton#103) + - '**' pull_request: permissions: From d72c6a081b67ce18eae654bf3c8d2d627af6939e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 Mar 2024 13:46:21 -0400 Subject: [PATCH 692/835] Fetch unshallow clones in readthedocs. Closes jaraco/skeleton#114. --- .readthedocs.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 68489063..85dfea9d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,3 +10,7 @@ build: os: ubuntu-lts-latest tools: python: latest + # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 + jobs: + post_checkout: + - git fetch --unshallow || true From 3fc7a935dfc0e5c8e330a29efc5518c464795cf8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Mar 2024 21:11:46 -0400 Subject: [PATCH 693/835] Move Python 3.11 out of the test matrix. Probably should have done this when moving continue-on-error to Python 3.13. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 143b0984..a15c74a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,6 @@ jobs: matrix: python: - "3.8" - - "3.11" - "3.12" platform: - ubuntu-latest @@ -45,6 +44,8 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest + - python: "3.11" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} From 6ff02e0eefcd90e271cefd326b460ecfa0e3eb9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Mar 2024 04:27:11 -0400 Subject: [PATCH 694/835] Configure pytest to support namespace packages. Ref pytest-dev/pytest#12112. --- pytest.ini | 5 ++++- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 022a723e..9a0f3bce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules +addopts= + --doctest-modules + --import-mode importlib +consider_namespace_packages=true filterwarnings= ## upstream diff --git a/setup.cfg b/setup.cfg index 6fa73b6a..f46b6cbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = [options.extras_require] testing = # upstream - pytest >= 6 + pytest >= 6, != 8.1.1 pytest-checkdocs >= 2.4 pytest-cov pytest-mypy From 0eddf59bd4d52d4f2e0925c27434d4bf58191663 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:11:23 -0400 Subject: [PATCH 695/835] Add type annotation for iterdir. Closes #220. --- newsfragments/220.bugfix.rst | 1 + path/__init__.pyi | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/220.bugfix.rst diff --git a/newsfragments/220.bugfix.rst b/newsfragments/220.bugfix.rst new file mode 100644 index 00000000..5b04804e --- /dev/null +++ b/newsfragments/220.bugfix.rst @@ -0,0 +1 @@ +Add type annotation for iterdir. diff --git a/path/__init__.pyi b/path/__init__.pyi index 1031f3e2..99642a8e 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -113,6 +113,7 @@ class Path(str): def relpath(self: Self, start: str = ...) -> Self: ... def relpathto(self: Self, dest: str) -> Self: ... # --- Listing, searching, walking, and matching + def iterdir(self: Self) -> Iterator[Self]: ... def listdir(self: Self, match: _Match = ...) -> List[Self]: ... def dirs(self: Self, match: _Match = ...) -> List[Self]: ... def files(self: Self, match: _Match = ...) -> List[Self]: ... From bbbd30c6baf9d5e3e99642a7be0529cdbe2eef9b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:49:20 -0400 Subject: [PATCH 696/835] =?UTF-8?q?=F0=9F=9A=A1=20Toil=20the=20docs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suppress nitpicky checks for missing typeshed definitions. --- docs/conf.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 93eecfb5..2d792ef0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,21 @@ # Be strict about any broken references nitpicky = True + +nitpick_ignore = [ + ('py:class', '_io.BufferedRandom'), + ('py:class', '_io.BufferedReader'), + ('py:class', '_io.BufferedWriter'), + ('py:class', '_io.FileIO'), + ('py:class', '_io.TextIOWrapper'), + ('py:class', 'Literal[-1, 1]'), + ('py:class', 'OpenBinaryMode'), + ('py:class', 'OpenBinaryModeReading'), + ('py:class', 'OpenBinaryModeUpdating'), + ('py:class', 'OpenBinaryModeWriting'), + ('py:class', 'OpenTextMode'), +] + # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] From 10f442674c1b6dcaa7fb68419ab8426cc59a36f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:54:01 -0400 Subject: [PATCH 697/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/220.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/220.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 430681b1..671b28d3 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.10.1 +======== + +Bugfixes +-------- + +- Add type annotation for iterdir. (#220) + + v16.10.0 ======== diff --git a/newsfragments/220.bugfix.rst b/newsfragments/220.bugfix.rst deleted file mode 100644 index 5b04804e..00000000 --- a/newsfragments/220.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add type annotation for iterdir. From a9bbe15fba309a3b400805986bb191f91fd255bd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:52:18 -0400 Subject: [PATCH 698/835] Fix iterdir - it also accepts match. Ref #220. --- newsfragments/220.bugfix.1.rst | 1 + path/__init__.pyi | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/220.bugfix.1.rst diff --git a/newsfragments/220.bugfix.1.rst b/newsfragments/220.bugfix.1.rst new file mode 100644 index 00000000..b0046edd --- /dev/null +++ b/newsfragments/220.bugfix.1.rst @@ -0,0 +1 @@ +Fix iterdir - it also accepts match. Ref #220. \ No newline at end of file diff --git a/path/__init__.pyi b/path/__init__.pyi index 99642a8e..c8193e6b 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -113,7 +113,7 @@ class Path(str): def relpath(self: Self, start: str = ...) -> Self: ... def relpathto(self: Self, dest: str) -> Self: ... # --- Listing, searching, walking, and matching - def iterdir(self: Self) -> Iterator[Self]: ... + def iterdir(self: Self, match: _Match = ...) -> Iterator[Self]: ... def listdir(self: Self, match: _Match = ...) -> List[Self]: ... def dirs(self: Self, match: _Match = ...) -> List[Self]: ... def files(self: Self, match: _Match = ...) -> List[Self]: ... From c8b082cd9b4cd9230bd3b2b3aeb700ad444657e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:54:42 -0400 Subject: [PATCH 699/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/220.bugfix.1.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/220.bugfix.1.rst diff --git a/NEWS.rst b/NEWS.rst index 671b28d3..818d28d2 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.10.2 +======== + +Bugfixes +-------- + +- Fix iterdir - it also accepts match. Ref #220. (#220) + + v16.10.1 ======== diff --git a/newsfragments/220.bugfix.1.rst b/newsfragments/220.bugfix.1.rst deleted file mode 100644 index b0046edd..00000000 --- a/newsfragments/220.bugfix.1.rst +++ /dev/null @@ -1 +0,0 @@ -Fix iterdir - it also accepts match. Ref #220. \ No newline at end of file From e608ef69f723e8bd8088f0cbdcdba4bc62fad5c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 05:58:53 -0400 Subject: [PATCH 700/835] Add news fragment. --- newsfragments/215.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/215.feature.rst diff --git a/newsfragments/215.feature.rst b/newsfragments/215.feature.rst new file mode 100644 index 00000000..da92620c --- /dev/null +++ b/newsfragments/215.feature.rst @@ -0,0 +1 @@ +Inlined some types. \ No newline at end of file From 8f325dcd0df509fe3e4b1017d67dfc91c715940d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:04:57 -0400 Subject: [PATCH 701/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/215.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/215.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 818d28d2..0d96987a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.11.0 +======== + +Features +-------- + +- Inlined some types. (#215) + + v16.10.2 ======== diff --git a/newsfragments/215.feature.rst b/newsfragments/215.feature.rst deleted file mode 100644 index da92620c..00000000 --- a/newsfragments/215.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Inlined some types. \ No newline at end of file From 6abd966aa41006de0ab4de42c154eee817236c25 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:13:04 -0400 Subject: [PATCH 702/835] Added .is_dir and .is_file for parity with pathlib. Ref #214 --- newsfragments/214.feature.rst | 1 + path/__init__.py | 38 ++++++++++++++++++++--------- path/__init__.pyi | 2 ++ test_path.py | 46 +++++++++++++++++------------------ 4 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 newsfragments/214.feature.rst diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst new file mode 100644 index 00000000..a0bde559 --- /dev/null +++ b/newsfragments/214.feature.rst @@ -0,0 +1 @@ +Added .is_dir and .is_file for parity with pathlib. Deprecates .isdir and .isfile. diff --git a/path/__init__.py b/path/__init__.py index 40da95a4..30c1494e 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -125,7 +125,7 @@ class Traversal: Directories beginning with `.` will appear in the results, but their children will not. - >>> dot_dir = next(item for item in items if item.isdir() and item.startswith('.')) + >>> dot_dir = next(item for item in items if item.is_dir() and item.startswith('.')) >>> any(item.parent == dot_dir for item in items) False """ @@ -566,7 +566,7 @@ def dirs(self, *args, **kwargs): Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.iterdir(*args, **kwargs) if p.isdir()] + return [p for p in self.iterdir(*args, **kwargs) if p.is_dir()] def files(self, *args, **kwargs): """List of the files in self. @@ -577,14 +577,14 @@ def files(self, *args, **kwargs): Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.iterdir(*args, **kwargs) if p.isfile()] + return [p for p in self.iterdir(*args, **kwargs) if p.is_file()] def walk(self, match=None, errors='strict'): """Iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of this directory and its descendants. This requires that - ``D.isdir()``. + ``D.is_dir()``. This performs a depth-first traversal of the directory tree. Each directory is returned just before all its children. @@ -609,7 +609,7 @@ def walk(self, match=None, errors='strict'): traverse = None if match(child): traverse = yield child - traverse = traverse or child.isdir + traverse = traverse or child.is_dir try: do_traverse = traverse() except Exception as exc: @@ -621,11 +621,11 @@ def walk(self, match=None, errors='strict'): def walkdirs(self, *args, **kwargs): """Iterator over subdirs, recursively.""" - return (item for item in self.walk(*args, **kwargs) if item.isdir()) + return (item for item in self.walk(*args, **kwargs) if item.is_dir()) def walkfiles(self, *args, **kwargs): """Iterator over files, recursively.""" - return (item for item in self.walk(*args, **kwargs) if item.isfile()) + return (item for item in self.walk(*args, **kwargs) if item.is_file()) def fnmatch(self, pattern, normcase=None): """Return ``True`` if `self.name` matches the given `pattern`. @@ -1097,10 +1097,24 @@ def exists(self): return self.module.exists(self) def isdir(self): + warnings.warn( + "isdir is deprecated; use is_dir", + DeprecationWarning, + stacklevel=2, + ) + + def is_dir(self): """.. seealso:: :func:`os.path.isdir`""" return self.module.isdir(self) def isfile(self): + warnings.warn( + "isfile is deprecated; use is_file", + DeprecationWarning, + stacklevel=2, + ) + + def is_file(self): """.. seealso:: :func:`os.path.isfile`""" return self.module.isfile(self) @@ -1556,7 +1570,7 @@ def ignored(item): if symlinks and source.islink(): target = source.readlink() target.symlink(dest) - elif source.isdir(): + elif source.is_dir(): source.merge_tree( dest, symlinks=symlinks, @@ -1613,7 +1627,7 @@ def in_place( For example, to add line numbers to a file:: p = Path(filename) - assert p.isfile() + assert p.is_file() with p.in_place() as (reader, writer): for number, line in enumerate(reader, 1): writer.write('{0:3}: '.format(number))) @@ -1747,7 +1761,7 @@ class ExtantFile(Path): """ def _validate(self): - if not self.isfile(): + if not self.is_file(): raise FileNotFoundError(f"{self} does not exist as a file.") @@ -1818,12 +1832,12 @@ class TempDir(Path): For example: >>> with TempDir() as d: - ... d.isdir() and isinstance(d, Path) + ... d.is_dir() and isinstance(d, Path) True The directory is deleted automatically. - >>> d.isdir() + >>> d.is_dir() False .. seealso:: :func:`tempfile.mkdtemp` diff --git a/path/__init__.pyi b/path/__init__.pyi index 8a779d51..2e8a926f 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -154,7 +154,9 @@ class Path(str): def isabs(self) -> bool: ... def exists(self) -> bool: ... def isdir(self) -> bool: ... + def is_dir(self) -> bool: ... def isfile(self) -> bool: ... + def is_file(self) -> bool: ... def islink(self) -> bool: ... def ismount(self) -> bool: ... def samefile(self, other: str) -> bool: ... diff --git a/test_path.py b/test_path.py index 6dbe143b..6b6f316c 100644 --- a/test_path.py +++ b/test_path.py @@ -300,10 +300,10 @@ def test_removedirs_p(self, tmpdir): (dir / 'file').touch() (dir / 'sub').mkdir() dir.removedirs_p() - assert dir.isdir() - assert (dir / 'file').isfile() + assert dir.is_dir() + assert (dir / 'file').is_file() # TODO: shouldn't sub get removed? - # assert not (dir / 'sub').isdir() + # assert not (dir / 'sub').is_dir() class TestReadWriteText: @@ -360,7 +360,7 @@ def test_symlink_none(self, tmpdir): with root: file = (Path('dir').mkdir() / 'file').touch() file.symlink() - assert Path('file').isfile() + assert Path('file').is_file() def test_readlinkabs_passthrough(self, tmpdir): link = Path(tmpdir) / 'link' @@ -383,7 +383,7 @@ def test_skip_symlinks(self, tmpdir): assert len(list(root.walk())) == 4 skip_links = path.Traversal( - lambda item: item.isdir() and not item.islink(), + lambda item: item.is_dir() and not item.islink(), ) assert len(list(skip_links(root.walk()))) == 3 @@ -474,7 +474,7 @@ def test_touch(self, tmpdir): t1 = time.time() + threshold assert f.exists() - assert f.isfile() + assert f.is_file() assert f.size == 0 assert t0 <= f.mtime <= t1 if hasattr(os.path, 'getctime'): @@ -493,7 +493,7 @@ def test_touch(self, tmpdir): assert t0 <= t1 < t2 <= t3 # sanity check assert f.exists() - assert f.isfile() + assert f.is_file() assert f.size == 10 assert t2 <= f.mtime <= t3 if hasattr(os.path, 'getctime'): @@ -593,7 +593,7 @@ def test_makedirs(self, tmpdir): boz = foo / 'bar' / 'baz' / 'boz' boz.makedirs() try: - assert boz.isdir() + assert boz.is_dir() finally: boz.removedirs() assert not foo.exists() @@ -602,7 +602,7 @@ def test_makedirs(self, tmpdir): foo.mkdir(0o750) boz.makedirs(0o700) try: - assert boz.isdir() + assert boz.is_dir() finally: boz.removedirs() assert not foo.exists() @@ -648,13 +648,13 @@ def test_shutil(self, tmpdir): # Test simple file copying. testFile.copyfile(testCopy) - assert testCopy.isfile() + assert testCopy.is_file() assert testFile.bytes() == testCopy.bytes() # Test copying into a directory. testCopy2 = testA / testFile.name testFile.copy(testA) - assert testCopy2.isfile() + assert testCopy2.is_file() assert testFile.bytes() == testCopy2.bytes() # Make a link for the next test to use. @@ -662,7 +662,7 @@ def test_shutil(self, tmpdir): # Test copying directory tree. testA.copytree(testC) - assert testC.isdir() + assert testC.is_dir() self.assertSetsEqual( testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], @@ -675,7 +675,7 @@ def test_shutil(self, tmpdir): # Copy again, preserving symlinks. testA.copytree(testC, True) - assert testC.isdir() + assert testC.is_dir() self.assertSetsEqual( testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], @@ -698,7 +698,7 @@ def test_patterns(self, tmpdir): dirs = [d, d / 'xdir', d / 'xdir.tmp', d / 'xdir.tmp' / 'xsubdir'] for e in dirs: - if not e.isdir(): + if not e.is_dir(): e.makedirs() for name in names: @@ -913,12 +913,12 @@ def testing_structure(self, tmpdir): def check_link(self): target = Path(self.subdir_b / self.test_link.name) - check = target.islink if hasattr(os, 'symlink') else target.isfile + check = target.islink if hasattr(os, 'symlink') else target.is_file assert check() def test_with_nonexisting_dst_kwargs(self): self.subdir_a.merge_tree(self.subdir_b, symlinks=True) - assert self.subdir_b.isdir() + assert self.subdir_b.is_dir() expected = { self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, @@ -928,7 +928,7 @@ def test_with_nonexisting_dst_kwargs(self): def test_with_nonexisting_dst_args(self): self.subdir_a.merge_tree(self.subdir_b, True) - assert self.subdir_b.isdir() + assert self.subdir_b.is_dir() expected = { self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, @@ -948,7 +948,7 @@ def test_with_existing_dst(self): self.subdir_a.merge_tree(self.subdir_b, True) - assert self.subdir_b.isdir() + assert self.subdir_b.is_dir() expected = { self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, @@ -965,7 +965,7 @@ def test_copytree_parameters(self): ignore = shutil.ignore_patterns('testlink*') self.subdir_a.merge_tree(self.subdir_b, ignore=ignore) - assert self.subdir_b.isdir() + assert self.subdir_b.is_dir() assert list(self.subdir_b.iterdir()) == [self.subdir_b / self.test_file.name] def test_only_newer(self): @@ -984,7 +984,7 @@ def test_only_newer(self): def test_nested(self): self.subdir_a.joinpath('subsub').mkdir() self.subdir_a.merge_tree(self.subdir_b) - assert self.subdir_b.joinpath('subsub').isdir() + assert self.subdir_b.joinpath('subsub').is_dir() def test_listdir(self): with pytest.deprecated_call(): @@ -1040,7 +1040,7 @@ def test_constructor(self): d = TempDir() assert isinstance(d, path.Path) assert d.exists() - assert d.isdir() + assert d.is_dir() d.rmdir() assert not d.exists() @@ -1074,8 +1074,8 @@ def test_context_manager_using_with(self): """ with TempDir() as d: - assert d.isdir() - assert not d.isdir() + assert d.is_dir() + assert not d.is_dir() def test_cleaned_up_on_interrupt(self): with contextlib.suppress(KeyboardInterrupt): From 90c53c886fc1b41846de3951dd3a2d30cc75c1e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:15:19 -0400 Subject: [PATCH 703/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/214.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/214.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 0d96987a..b22b13e3 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.12.0 +======== + +Features +-------- + +- Added .is_dir and .is_file for parity with pathlib. Deprecates .isdir and .isfile. (#214) + + v16.11.0 ======== diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst deleted file mode 100644 index a0bde559..00000000 --- a/newsfragments/214.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added .is_dir and .is_file for parity with pathlib. Deprecates .isdir and .isfile. From 1650151451e8b2d4894f6f49d9cace36aca3bf7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:20:49 -0400 Subject: [PATCH 704/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied I,B,UP fixes from Ruff. --- path/__init__.py | 114 ++++++++++++++++++------------------- path/__init__.pyi | 135 +++++++++++++++++++++----------------------- path/classes.pyi | 4 +- path/compat/py38.py | 1 - path/masks.py | 5 +- path/matchers.py | 4 +- test_path.py | 30 +++++----- 7 files changed, 135 insertions(+), 158 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 30c1494e..942cd7b7 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -25,23 +25,22 @@ from __future__ import annotations import builtins -import sys -import warnings -import os +import contextlib +import datetime +import errno import fnmatch +import functools import glob -import shutil import hashlib -import errno -import tempfile -import functools -import re -import contextlib import importlib import itertools -import datetime +import os +import re +import shutil +import sys +import tempfile +import warnings from numbers import Number -from typing import Union with contextlib.suppress(ImportError): import win32security @@ -60,33 +59,28 @@ TextIOWrapper, ) from typing import ( + IO, + TYPE_CHECKING, Any, BinaryIO, Callable, - IO, Iterator, - Optional, overload, ) -from typing import TYPE_CHECKING - if TYPE_CHECKING: from _typeshed import ( OpenBinaryMode, - OpenBinaryModeUpdating, OpenBinaryModeReading, + OpenBinaryModeUpdating, OpenBinaryModeWriting, OpenTextMode, ) from typing_extensions import Literal -from . import matchers -from . import masks -from . import classes +from . import classes, masks, matchers from .compat.py38 import removesuffix - __all__ = ['Path', 'TempDir'] @@ -100,7 +94,7 @@ _default_linesep = object() -def _make_timestamp_ns(value: Union[Number, datetime.datetime]) -> Number: +def _make_timestamp_ns(value: Number | datetime.datetime) -> Number: timestamp_s = value if isinstance(value, Number) else value.timestamp() return int(timestamp_s * 10**9) @@ -203,7 +197,7 @@ def _next_class(cls): # --- Special Python methods. def __repr__(self): - return '{}({})'.format(type(self).__name__, super().__repr__()) + return f'{type(self).__name__}({super().__repr__()})' # Adding a Path and a string yields a Path. def __add__(self, more): @@ -689,11 +683,11 @@ def open( self, mode: OpenTextMode = ..., buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., + opener: Callable[[str, int], int] | None = ..., ) -> TextIOWrapper: ... @overload @@ -701,9 +695,9 @@ def open( self, mode: OpenBinaryMode, buffering: Literal[0], - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> FileIO: ... @@ -713,9 +707,9 @@ def open( self, mode: OpenBinaryModeUpdating, buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> BufferedRandom: ... @@ -725,9 +719,9 @@ def open( self, mode: OpenBinaryModeReading, buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> BufferedReader: ... @@ -737,9 +731,9 @@ def open( self, mode: OpenBinaryModeWriting, buffering: Literal[-1, 1] = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> BufferedWriter: ... @@ -749,9 +743,9 @@ def open( self, mode: OpenBinaryMode, buffering: int, - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> BinaryIO: ... @@ -761,9 +755,9 @@ def open( self, mode: str, buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., ) -> IO[Any]: ... @@ -787,11 +781,11 @@ def chunks( size: int, mode: OpenTextMode = ..., buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., + opener: Callable[[str, int], int] | None = ..., ) -> Iterator[str]: ... @overload @@ -800,11 +794,11 @@ def chunks( size: int, mode: OpenBinaryMode, buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., + opener: Callable[[str, int], int] | None = ..., ) -> Iterator[builtins.bytes]: ... @overload @@ -813,12 +807,12 @@ def chunks( size: int, mode: str, buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., closefd: bool = ..., - opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[Union[str, builtins.bytes]]: ... + opener: Callable[[str, int], int] | None = ..., + ) -> Iterator[str | builtins.bytes]: ... def chunks(self, size, *args, **kwargs): """Returns a generator yielding chunks of the file, so it can @@ -877,9 +871,9 @@ def text(self, encoding=None, errors='strict'): def write_text( self, text: str, - encoding: Optional[str] = ..., + encoding: str | None = ..., errors: str = ..., - linesep: Optional[str] = ..., + linesep: str | None = ..., append: bool = ..., ) -> None: ... @@ -889,7 +883,7 @@ def write_text( text: builtins.bytes, encoding: None = ..., errors: str = ..., - linesep: Optional[str] = ..., + linesep: str | None = ..., append: bool = ..., ) -> None: ... diff --git a/path/__init__.pyi b/path/__init__.pyi index 2e8a926f..95ea6326 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -6,19 +6,14 @@ import os import sys from types import ModuleType, TracebackType from typing import ( + IO, Any, AnyStr, Callable, Generator, Iterable, Iterator, - IO, - List, Optional, - Set, - Tuple, - Type, - Union, ) from _typeshed import ( @@ -28,7 +23,7 @@ from _typeshed import ( from . import classes # Type for the match argument for several methods -_Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] +_Match = Optional[str | Callable[[str], bool] | Callable[[Path], bool]] class TreeWalkWarning(Warning): pass @@ -39,7 +34,7 @@ class Traversal: def __init__(self, follow: Callable[[Path], bool]): ... def __call__( self, - walker: Generator[Path, Optional[Callable[[], bool]], None], + walker: Generator[Path, Callable[[], bool] | None, None], ) -> Iterator[Path]: ... class Path(str): @@ -47,10 +42,10 @@ class Path(str): def __init__(self, other: Any = ...) -> None: ... @classmethod - def using_module(cls, module: ModuleType) -> Type[Path]: ... + def using_module(cls, module: ModuleType) -> type[Path]: ... @classes.ClassProperty @classmethod - def _next_class(cls: Type[Self]) -> Type[Self]: ... + def _next_class(cls: type[Self]) -> type[Self]: ... def __repr__(self) -> str: ... def __add__(self: Self, more: str) -> Self: ... def __radd__(self: Self, other: str) -> Self: ... @@ -61,12 +56,12 @@ class Path(str): def __enter__(self: Self) -> Self: ... def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: ... @classmethod - def getcwd(cls: Type[Self]) -> Self: ... + def getcwd(cls: type[Self]) -> Self: ... def abspath(self: Self) -> Self: ... def normcase(self: Self) -> Self: ... def normpath(self: Self) -> Self: ... @@ -87,29 +82,29 @@ class Path(str): def parent(self: Self) -> Self: ... @property def name(self: Self) -> Self: ... - def splitpath(self: Self) -> Tuple[Self, str]: ... - def splitdrive(self: Self) -> Tuple[Self, Self]: ... - def splitext(self: Self) -> Tuple[Self, str]: ... + def splitpath(self: Self) -> tuple[Self, str]: ... + def splitdrive(self: Self) -> tuple[Self, Self]: ... + def splitext(self: Self) -> tuple[Self, str]: ... def stripext(self: Self) -> Self: ... @classes.multimethod def joinpath(cls: Self, first: str, *others: str) -> Self: ... - def splitall(self: Self) -> List[Union[Self, str]]: ... - def parts(self: Self) -> Tuple[Union[Self, str], ...]: ... - def _parts(self: Self) -> Iterator[Union[Self, str]]: ... - def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: ... + def splitall(self: Self) -> list[Self | str]: ... + def parts(self: Self) -> tuple[Self | str, ...]: ... + def _parts(self: Self) -> Iterator[Self | str]: ... + def _parts_iter(self: Self) -> Iterator[Self | str]: ... def relpath(self: Self, start: str = ...) -> Self: ... def relpathto(self: Self, dest: str) -> Self: ... # --- Listing, searching, walking, and matching def iterdir(self: Self, match: _Match = ...) -> Iterator[Self]: ... - def listdir(self: Self, match: _Match = ...) -> List[Self]: ... - def dirs(self: Self, match: _Match = ...) -> List[Self]: ... - def files(self: Self, match: _Match = ...) -> List[Self]: ... + def listdir(self: Self, match: _Match = ...) -> list[Self]: ... + def dirs(self: Self, match: _Match = ...) -> list[Self]: ... + def files(self: Self, match: _Match = ...) -> list[Self]: ... def walk( self: Self, match: _Match = ..., errors: str = ..., - ) -> Generator[Self, Optional[Callable[[], bool]], None]: ... + ) -> Generator[Self, Callable[[], bool] | None, None]: ... def walkdirs( self: Self, match: _Match = ..., @@ -122,30 +117,30 @@ class Path(str): ) -> Iterator[Self]: ... def fnmatch( self, - pattern: Union[Path, str], - normcase: Optional[Callable[[str], str]] = ..., + pattern: Path | str, + normcase: Callable[[str], str] | None = ..., ) -> bool: ... - def glob(self: Self, pattern: str) -> List[Self]: ... + def glob(self: Self, pattern: str) -> list[Self]: ... def iglob(self: Self, pattern: str) -> Iterator[Self]: ... def bytes(self) -> builtins.bytes: ... def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... def read_text( - self, encoding: Optional[str] = ..., errors: Optional[str] = ... + self, encoding: str | None = ..., errors: str | None = ... ) -> str: ... def read_bytes(self) -> builtins.bytes: ... - def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: ... + def text(self, encoding: str | None = ..., errors: str = ...) -> str: ... def lines( self, - encoding: Optional[str] = ..., - errors: Optional[str] = ..., + encoding: str | None = ..., + errors: str | None = ..., retain: bool = ..., - ) -> List[str]: ... + ) -> list[str]: ... def write_lines( self, - lines: List[str], - encoding: Optional[str] = ..., + lines: list[str], + encoding: str | None = ..., errors: str = ..., - linesep: Optional[str] = ..., + linesep: str | None = ..., append: bool = ..., ) -> None: ... def read_md5(self) -> builtins.bytes: ... @@ -176,7 +171,7 @@ class Path(str): self, mode: int, *, - dir_fd: Optional[int] = ..., + dir_fd: int | None = ..., effective_ids: bool = ..., follow_symlinks: bool = ..., ) -> bool: ... @@ -188,22 +183,20 @@ class Path(str): if sys.platform != 'win32': def statvfs(self) -> os.statvfs_result: ... - def pathconf(self, name: Union[str, int]) -> int: ... + def pathconf(self, name: str | int) -> int: ... def utime( self, - times: Union[Tuple[int, int], Tuple[float, float], None] = ..., + times: tuple[int, int] | tuple[float, float] | None = ..., *, - ns: Tuple[int, int] = ..., - dir_fd: Optional[int] = ..., + ns: tuple[int, int] = ..., + dir_fd: int | None = ..., follow_symlinks: bool = ..., ) -> Path: ... - def chmod(self: Self, mode: Union[str, int]) -> Self: ... + def chmod(self: Self, mode: str | int) -> Self: ... if sys.platform != 'win32': - def chown( - self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ... - ) -> Self: ... + def chown(self: Self, uid: int | str = ..., gid: int | str = ...) -> Self: ... def rename(self: Self, new: str) -> Self: ... def renames(self: Self, new: str) -> Self: ... @@ -221,7 +214,7 @@ class Path(str): def unlink(self: Self) -> Self: ... def unlink_p(self: Self) -> Self: ... def link(self: Self, newpath: str) -> Self: ... - def symlink(self: Self, newlink: Optional[str] = ...) -> Self: ... + def symlink(self: Self, newlink: str | None = ...) -> Self: ... def readlink(self: Self) -> Self: ... def readlinkabs(self: Self) -> Self: ... def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... @@ -233,7 +226,7 @@ class Path(str): self, dst: str, symlinks: bool = ..., - ignore: Optional[Callable[[str, list[str]], Iterable[str]]] = ..., + ignore: Callable[[str, list[str]], Iterable[str]] | None = ..., copy_function: Callable[[str, str], None] = ..., ignore_dangling_symlinks: bool = ..., dirs_exist_ok: bool = ..., @@ -244,7 +237,7 @@ class Path(str): def rmtree( self, ignore_errors: bool = ..., - onerror: Optional[Callable[[Any, Any, Any], Any]] = ..., + onerror: Callable[[Any, Any, Any], Any] | None = ..., ) -> None: ... def rmtree_p(self: Self) -> Self: ... def chdir(self) -> None: ... @@ -255,28 +248,28 @@ class Path(str): symlinks: bool = ..., *, copy_function: Callable[[str, str], None] = ..., - ignore: Callable[[Any, List[str]], Union[List[str], Set[str]]] = ..., + ignore: Callable[[Any, list[str]], list[str] | set[str]] = ..., ) -> None: ... if sys.platform != 'win32': def chroot(self) -> None: ... if sys.platform == 'win32': - def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... + def startfile(self: Self, operation: str | None = ...) -> Self: ... @contextlib.contextmanager def in_place( self, mode: str = ..., buffering: int = ..., - encoding: Optional[str] = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - backup_extension: Optional[str] = ..., - ) -> Iterator[Tuple[IO[Any], IO[Any]]]: ... + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + backup_extension: str | None = ..., + ) -> Iterator[tuple[IO[Any], IO[Any]]]: ... @classes.ClassProperty @classmethod - def special(cls) -> Callable[[Optional[str]], SpecialResolver]: ... + def special(cls) -> Callable[[str | None], SpecialResolver]: ... class DirectoryNotEmpty(OSError): @staticmethod @@ -298,9 +291,9 @@ class SpecialResolver: def __init__( self, path_class: type, - appname: Optional[str] = ..., - appauthor: Optional[str] = ..., - version: Optional[str] = ..., + appname: str | None = ..., + appauthor: str | None = ..., + version: str | None = ..., roaming: bool = ..., multipath: bool = ..., ): ... @@ -309,13 +302,13 @@ class SpecialResolver: class Multi: @classmethod - def for_class(cls, path_cls: type) -> Type[MultiPathType]: ... + def for_class(cls, path_cls: type) -> type[MultiPathType]: ... @classmethod def detect(cls, input: str) -> MultiPathType: ... def __iter__(self) -> Iterator[Path]: ... @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: ... + def _next_class(cls) -> type[Path]: ... class MultiPathType(Multi, Path): pass @@ -323,24 +316,22 @@ class MultiPathType(Multi, Path): class TempDir(Path): @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: ... + def _next_class(cls) -> type[Path]: ... def __new__( - cls: Type[Self], - suffix: Optional[AnyStr] = ..., - prefix: Optional[AnyStr] = ..., - dir: Optional[Union[AnyStr, os.PathLike[AnyStr]]] = ..., + cls: type[Self], + suffix: AnyStr | None = ..., + prefix: AnyStr | None = ..., + dir: AnyStr | os.PathLike[AnyStr] | None = ..., ) -> Self: ... def __init__(self) -> None: ... def __enter__(self) -> Path: ... # type: ignore def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: ... class Handlers: @classmethod - def _resolve( - cls, param: Union[str, Callable[[str], None]] - ) -> Callable[[str], None]: ... + def _resolve(cls, param: str | Callable[[str], None]) -> Callable[[str], None]: ... diff --git a/path/classes.pyi b/path/classes.pyi index 2878c48b..0e119d0b 100644 --- a/path/classes.pyi +++ b/path/classes.pyi @@ -1,8 +1,8 @@ from typing import Any, Callable, Optional class ClassProperty(property): - def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: ... + def __get__(self, cls: Any, owner: type | None = ...) -> Any: ... class multimethod: def __init__(self, func: Callable[..., Any]): ... - def __get__(self, instance: Any, owner: Optional[type]) -> Any: ... + def __get__(self, instance: Any, owner: type | None) -> Any: ... diff --git a/path/compat/py38.py b/path/compat/py38.py index a5f0fc1c..ed7bd0ac 100644 --- a/path/compat/py38.py +++ b/path/compat/py38.py @@ -1,6 +1,5 @@ import sys - if sys.version_info < (3, 9): def removesuffix(self, suffix): diff --git a/path/masks.py b/path/masks.py index 13ae00a3..f65a170f 100644 --- a/path/masks.py +++ b/path/masks.py @@ -1,8 +1,7 @@ -import re import functools -import operator import itertools - +import operator +import re # from jaraco.functools from typing import Any, Callable diff --git a/path/matchers.py b/path/matchers.py index 1c023897..20ca92e2 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -1,9 +1,7 @@ from __future__ import annotations -import ntpath import fnmatch - - +import ntpath from typing import Any, overload diff --git a/test_path.py b/test_path.py index 6b6f316c..3cc6f0d9 100644 --- a/test_path.py +++ b/test_path.py @@ -13,31 +13,27 @@ time on files. """ -import os -import sys -import shutil -import time -import types +import contextlib +import datetime +import importlib import ntpath -import posixpath -import textwrap +import os import platform -import importlib -import datetime -import subprocess +import posixpath import re -import contextlib +import shutil import stat +import subprocess +import sys +import textwrap +import time +import types import pytest from more_itertools import ilen import path -from path import Path -from path import TempDir -from path import matchers -from path import SpecialResolver -from path import Multi +from path import Multi, Path, SpecialResolver, TempDir, matchers def os_choose(**choices): @@ -1092,7 +1088,7 @@ def unicode_name_in_tmpdir(self, tmpdir): Path(tmpdir).joinpath('☃').mkdir() def test_walkdirs_with_unicode_name(self, tmpdir): - for res in Path(tmpdir).walkdirs(): + for _res in Path(tmpdir).walkdirs(): pass From cf71b38498a6681fae1d4c2e26af7a71ef6c66c4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:24:24 -0400 Subject: [PATCH 705/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index 95ea6326..8f6805ff 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -13,7 +13,6 @@ from typing import ( Generator, Iterable, Iterator, - Optional, ) from _typeshed import ( @@ -23,7 +22,7 @@ from _typeshed import ( from . import classes # Type for the match argument for several methods -_Match = Optional[str | Callable[[str], bool] | Callable[[Path], bool]] +_Match = str | Callable[[str], bool] | Callable[[Path], bool] | None class TreeWalkWarning(Warning): pass From 7f5d13873983ed797100ef2ff8f35e41e3583123 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:27:11 -0400 Subject: [PATCH 706/835] Fix some issues flagged by ruff --- path/__init__.py | 2 +- test_path.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 942cd7b7..f5b03a34 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1865,7 +1865,7 @@ def strict(msg): raise def warn(msg): - warnings.warn(msg, TreeWalkWarning) + warnings.warn(msg, TreeWalkWarning, stacklevel=2) def ignore(msg): pass diff --git a/test_path.py b/test_path.py index 3cc6f0d9..c0bdd64f 100644 --- a/test_path.py +++ b/test_path.py @@ -361,12 +361,12 @@ def test_symlink_none(self, tmpdir): def test_readlinkabs_passthrough(self, tmpdir): link = Path(tmpdir) / 'link' Path('foo').abspath().symlink(link) - link.readlinkabs() == Path('foo').abspath() + assert link.readlinkabs() == Path('foo').abspath() def test_readlinkabs_rendered(self, tmpdir): link = Path(tmpdir) / 'link' Path('foo').symlink(link) - link.readlinkabs() == Path(tmpdir) / 'foo' + assert link.readlinkabs() == Path(tmpdir) / 'foo' class TestSymbolicLinksWalk: @@ -559,7 +559,7 @@ def bytes_filename(self, tmpdir): with open(os.path.join(base, name), 'wb'): pass except Exception as exc: - raise pytest.skip(f"Invalid encodings disallowed {exc}") + raise pytest.skip(f"Invalid encodings disallowed {exc}") from exc return name def test_iterdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover From 518f406c570d9d8393f2129429f93a796ac8fc2e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:29:11 -0400 Subject: [PATCH 707/835] Mark tests as uncovered. --- path/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index f5b03a34..14931082 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -319,7 +319,7 @@ def suffix(self): return suffix @property - def ext(self): + def ext(self): # pragma: no cover warnings.warn( ".ext is deprecated; use suffix", DeprecationWarning, @@ -1090,7 +1090,7 @@ def exists(self): """.. seealso:: :func:`os.path.exists`""" return self.module.exists(self) - def isdir(self): + def isdir(self): # pragma: no cover warnings.warn( "isdir is deprecated; use is_dir", DeprecationWarning, @@ -1101,7 +1101,7 @@ def is_dir(self): """.. seealso:: :func:`os.path.isdir`""" return self.module.isdir(self) - def isfile(self): + def isfile(self): # pragma: no cover warnings.warn( "isfile is deprecated; use is_file", DeprecationWarning, From 117771380b961ad2b8626da3969775208e647673 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:30:13 -0400 Subject: [PATCH 708/835] Restore functionality in .isdir and .isfile. --- newsfragments/+d562a3d9.bugfix.rst | 1 + path/__init__.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 newsfragments/+d562a3d9.bugfix.rst diff --git a/newsfragments/+d562a3d9.bugfix.rst b/newsfragments/+d562a3d9.bugfix.rst new file mode 100644 index 00000000..21569f8e --- /dev/null +++ b/newsfragments/+d562a3d9.bugfix.rst @@ -0,0 +1 @@ +Restore functionality in .isdir and .isfile. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 30c1494e..513d71a4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1102,6 +1102,7 @@ def isdir(self): DeprecationWarning, stacklevel=2, ) + return self.is_dir() def is_dir(self): """.. seealso:: :func:`os.path.isdir`""" @@ -1113,6 +1114,7 @@ def isfile(self): DeprecationWarning, stacklevel=2, ) + return self.is_file() def is_file(self): """.. seealso:: :func:`os.path.isfile`""" From 74ebe6a722c409c45bc13f49709111034b7a38f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Apr 2024 06:30:26 -0400 Subject: [PATCH 709/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+d562a3d9.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+d562a3d9.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index b22b13e3..cf307bea 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v16.12.1 +======== + +Bugfixes +-------- + +- Restore functionality in .isdir and .isfile. + + v16.12.0 ======== diff --git a/newsfragments/+d562a3d9.bugfix.rst b/newsfragments/+d562a3d9.bugfix.rst deleted file mode 100644 index 21569f8e..00000000 --- a/newsfragments/+d562a3d9.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restore functionality in .isdir and .isfile. \ No newline at end of file From b991fd9eac11dff256315260cbfed00db8e98528 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 03:04:46 -0400 Subject: [PATCH 710/835] In readlink, prefer the display path to the substitute path. Closes #222 --- newsfragments/222.feature.rst | 1 + path/__init__.py | 4 ++-- path/compat/py38.py | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 newsfragments/222.feature.rst diff --git a/newsfragments/222.feature.rst b/newsfragments/222.feature.rst new file mode 100644 index 00000000..de669b73 --- /dev/null +++ b/newsfragments/222.feature.rst @@ -0,0 +1 @@ +In readlink, prefer the display path to the substitute path. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index df693ed9..07954b26 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -79,7 +79,7 @@ from typing_extensions import Literal from . import classes, masks, matchers -from .compat.py38 import removesuffix +from .compat.py38 import removeprefix, removesuffix __all__ = ['Path', 'TempDir'] @@ -1492,7 +1492,7 @@ def readlink(self): .. seealso:: :meth:`readlinkabs`, :func:`os.readlink` """ - return self._next_class(os.readlink(self)) + return self._next_class(removeprefix(os.readlink(self), '\\\\?\\')) def readlinkabs(self): """Return the path to which this symbolic link points. diff --git a/path/compat/py38.py b/path/compat/py38.py index ed7bd0ac..0af38140 100644 --- a/path/compat/py38.py +++ b/path/compat/py38.py @@ -8,7 +8,16 @@ def removesuffix(self, suffix): return self[: -len(suffix)] else: return self[:] + + def removeprefix(self, prefix): + if self.startswith(prefix): + return self[len(prefix) :] + else: + return self[:] else: def removesuffix(self, suffix): return self.removesuffix(suffix) + + def removeprefix(self, prefix): + return self.removeprefix(prefix) From e41031e9de7e4c5b2e2cc1f6887ad1cd30759c13 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 03:10:13 -0400 Subject: [PATCH 711/835] Create 'absolute' method and deprecate 'abspath'. Ref #214 --- newsfragments/214.feature.rst | 1 + path/__init__.py | 18 +++++++++++++----- test_path.py | 11 +++++------ 3 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 newsfragments/214.feature.rst diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst new file mode 100644 index 00000000..c1f241d0 --- /dev/null +++ b/newsfragments/214.feature.rst @@ -0,0 +1 @@ +Create 'absolute' method and deprecate 'abspath'. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 07954b26..ed778380 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -253,10 +253,18 @@ def getcwd(cls): # # --- Operations on Path strings. - def abspath(self): + def absolute(self): """.. seealso:: :func:`os.path.abspath`""" return self._next_class(self.module.abspath(self)) + def abspath(self): + warnings.warn( + ".abspath is deprecated; use absolute", + DeprecationWarning, + stacklevel=2, + ) + return self.absolute() + def normcase(self): """.. seealso:: :func:`os.path.normcase`""" return self._next_class(self.module.normcase(self)) @@ -492,10 +500,10 @@ def relpathto(self, dest): If there is no relative path from `self` to `dest`, for example if they reside on different drives in Windows, then this returns - ``dest.abspath()``. + ``dest.absolute()``. """ - origin = self.abspath() - dest = self._next_class(dest).abspath() + origin = self.absolute() + dest = self._next_class(dest).absolute() orig_list = origin.normcase().splitall() # Don't normcase dest! We want to preserve the case. @@ -1502,7 +1510,7 @@ def readlinkabs(self): .. seealso:: :meth:`readlink`, :func:`os.readlink` """ p = self.readlink() - return p if p.isabs() else (self.parent / p).abspath() + return p if p.isabs() else (self.parent / p).absolute() # High-level functions from shutil # These functions will be bound to the instance such that diff --git a/test_path.py b/test_path.py index c0bdd64f..35e0f479 100644 --- a/test_path.py +++ b/test_path.py @@ -134,11 +134,10 @@ def test_properties(self): # .drive assert f.drive == os_choose(nt='C:', posix='') - def test_methods(self): - # .abspath() - assert Path(os.curdir).abspath() == os.getcwd() + def test_absolute(self): + assert Path(os.curdir).absolute() == os.getcwd() - # .getcwd() + def test_getcwd(self): cwd = Path.getcwd() assert isinstance(cwd, Path) assert cwd == os.getcwd() @@ -360,8 +359,8 @@ def test_symlink_none(self, tmpdir): def test_readlinkabs_passthrough(self, tmpdir): link = Path(tmpdir) / 'link' - Path('foo').abspath().symlink(link) - assert link.readlinkabs() == Path('foo').abspath() + Path('foo').absolute().symlink(link) + assert link.readlinkabs() == Path('foo').absolute() def test_readlinkabs_rendered(self, tmpdir): link = Path(tmpdir) / 'link' From 6c1bc56436897dac6fc4941b644be6c3b21279e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 03:16:43 -0400 Subject: [PATCH 712/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/214.feature.rst | 1 - newsfragments/222.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/214.feature.rst delete mode 100644 newsfragments/222.feature.rst diff --git a/NEWS.rst b/NEWS.rst index cf307bea..f769bb47 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v16.13.0 +======== + +Features +-------- + +- Create 'absolute' method and deprecate 'abspath'. (#214) +- In readlink, prefer the display path to the substitute path. (#222) + + v16.12.1 ======== diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst deleted file mode 100644 index c1f241d0..00000000 --- a/newsfragments/214.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Create 'absolute' method and deprecate 'abspath'. \ No newline at end of file diff --git a/newsfragments/222.feature.rst b/newsfragments/222.feature.rst deleted file mode 100644 index de669b73..00000000 --- a/newsfragments/222.feature.rst +++ /dev/null @@ -1 +0,0 @@ -In readlink, prefer the display path to the substitute path. \ No newline at end of file From e0ff1841271f4602a4fbd164c3d0de9f20bf9fa0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 15:36:32 -0400 Subject: [PATCH 713/835] Add .cwd method and deprecated .getcwd. Ref #214 --- newsfragments/214.feature.rst | 1 + path/__init__.py | 13 +++++++++++-- test_path.py | 14 +++++++------- 3 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 newsfragments/214.feature.rst diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst new file mode 100644 index 00000000..479c95d9 --- /dev/null +++ b/newsfragments/214.feature.rst @@ -0,0 +1 @@ +Add .cwd method and deprecated .getcwd. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index ed778380..2412c947 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -235,7 +235,7 @@ def __rdiv__(self, rel): __rtruediv__ = __rdiv__ def __enter__(self): - self._old_dir = self.getcwd() + self._old_dir = self.cwd() os.chdir(self) return self @@ -243,13 +243,22 @@ def __exit__(self, *_): os.chdir(self._old_dir) @classmethod - def getcwd(cls): + def cwd(cls): """Return the current working directory as a path object. .. seealso:: :func:`os.getcwd` """ return cls(os.getcwd()) + @classmethod + def getcwd(cls): + warnings.warn( + ".getcwd is deprecated; use cwd", + DeprecationWarning, + stacklevel=2, + ) + return cls.cwd() + # # --- Operations on Path strings. diff --git a/test_path.py b/test_path.py index 35e0f479..6b1f6e3e 100644 --- a/test_path.py +++ b/test_path.py @@ -137,8 +137,8 @@ def test_properties(self): def test_absolute(self): assert Path(os.curdir).absolute() == os.getcwd() - def test_getcwd(self): - cwd = Path.getcwd() + def test_cwd(self): + cwd = Path.cwd() assert isinstance(cwd, Path) assert cwd == os.getcwd() @@ -990,7 +990,7 @@ class TestChdir: def test_chdir_or_cd(self, tmpdir): """tests the chdir or cd method""" d = Path(str(tmpdir)) - cwd = d.getcwd() + cwd = d.cwd() # ensure the cwd isn't our tempdir assert str(d) != str(cwd) @@ -998,18 +998,18 @@ def test_chdir_or_cd(self, tmpdir): d.chdir() # we now ensure that our cwd is the tempdir - assert str(d.getcwd()) == str(tmpdir) + assert str(d.cwd()) == str(tmpdir) # we're resetting our path d = Path(cwd) # we ensure that our cwd is still set to tempdir - assert str(d.getcwd()) == str(tmpdir) + assert str(d.cwd()) == str(tmpdir) # we're calling the alias cd method d.cd() # now, we ensure cwd isn'r tempdir - assert str(d.getcwd()) == str(cwd) - assert str(d.getcwd()) != str(tmpdir) + assert str(d.cwd()) == str(cwd) + assert str(d.cwd()) != str(tmpdir) class TestSubclass: From d702e7da5e7fb21138a34a03f18faeb93bc3b00a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 19:53:38 -0400 Subject: [PATCH 714/835] Add .symlink_to and .hardlink_to. Ref #214 --- newsfragments/214.feature.1.rst | 1 + path/__init__.py | 16 ++++++++++++++++ test_path.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 newsfragments/214.feature.1.rst diff --git a/newsfragments/214.feature.1.rst b/newsfragments/214.feature.1.rst new file mode 100644 index 00000000..1cb5185c --- /dev/null +++ b/newsfragments/214.feature.1.rst @@ -0,0 +1 @@ +Add .symlink_to and .hardlink_to. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 2412c947..6d2d8502 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1481,6 +1481,14 @@ def remove_p(self): # --- Links + def hardlink_to(self, target: str) -> None: + """ + Create a hard link at self, pointing to target. + + .. seealso:: :func:`os.link` + """ + os.link(target, self) + def link(self, newpath): """Create a hard link at `newpath`, pointing to this file. @@ -1489,6 +1497,14 @@ def link(self, newpath): os.link(self, newpath) return self._next_class(newpath) + def symlink_to(self, target: str, target_is_directory: bool = True) -> None: + """ + Create a symbolic link at self, pointing to target. + + .. seealso:: :func:`os.symlink` + """ + os.symlink(target, self, target_is_directory) + def symlink(self, newlink=None): """Create a symbolic link at `newlink`, pointing here. diff --git a/test_path.py b/test_path.py index 6b1f6e3e..1c5813f6 100644 --- a/test_path.py +++ b/test_path.py @@ -344,12 +344,26 @@ def test_get_owner(self): class TestLinks: + def test_hardlink_to(self, tmpdir): + target = Path(tmpdir) / 'target' + target.write_text('hello', encoding='utf-8') + link = Path(tmpdir).joinpath('link') + link.hardlink_to(target) + assert link.read_text(encoding='utf-8') == 'hello' + def test_link(self, tmpdir): target = Path(tmpdir) / 'target' target.write_text('hello', encoding='utf-8') link = target.link(Path(tmpdir) / 'link') assert link.read_text(encoding='utf-8') == 'hello' + def test_symlink_to(self, tmpdir): + target = Path(tmpdir) / 'target' + target.write_text('hello', encoding='utf-8') + link = Path(tmpdir).joinpath('link') + link.symlink_to(target) + assert link.read_text(encoding='utf-8') == 'hello' + def test_symlink_none(self, tmpdir): root = Path(tmpdir) with root: From c25980d97eafdc5957cb232e04506f17ccebc865 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 20:38:15 -0400 Subject: [PATCH 715/835] Fix incorrect default for target_is_directory. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 6d2d8502..cf008084 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1497,7 +1497,7 @@ def link(self, newpath): os.link(self, newpath) return self._next_class(newpath) - def symlink_to(self, target: str, target_is_directory: bool = True) -> None: + def symlink_to(self, target: str, target_is_directory: bool = False) -> None: """ Create a symbolic link at self, pointing to target. From 431e4a2290de10f5c238e11f3254b811df34f226 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 20:18:31 -0400 Subject: [PATCH 716/835] Add .group method. Ref #214 --- path/__init__.py | 13 +++++++++++-- test_path.py | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index cf008084..4520acae 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1259,7 +1259,7 @@ def access(self, *args, **kwargs): """ return os.access(self, *args, **kwargs) - def stat(self): + def stat(self, *, follow_symlinks=True): """ Perform a ``stat()`` system call on this path. @@ -1268,7 +1268,7 @@ def stat(self): .. seealso:: :meth:`lstat`, :func:`os.stat` """ - return os.stat(self) + return os.stat(self, follow_symlinks=follow_symlinks) def lstat(self): """ @@ -1327,6 +1327,15 @@ def __get_owner_not_implemented(self): # pragma: nocover .. seealso:: :meth:`get_owner`""", ) + if 'grp' in globals(): # pragma: no cover + + def group(self, *, follow_symlinks=True): + """ + Return the group name of the file gid. + """ + gid = self.stat(follow_symlinks=follow_symlinks).st_gid + return grp.getgrgid(gid).gr_name + if hasattr(os, 'statvfs'): def statvfs(self): diff --git a/test_path.py b/test_path.py index 1c5813f6..b0caacd2 100644 --- a/test_path.py +++ b/test_path.py @@ -300,6 +300,11 @@ def test_removedirs_p(self, tmpdir): # TODO: shouldn't sub get removed? # assert not (dir / 'sub').is_dir() + @pytest.mark.skipif("not hasattr(Path, 'group')") + def test_group(self, tmpdir): + file = Path(tmpdir).joinpath('file').touch() + assert isinstance(file.group(), str) + class TestReadWriteText: def test_read_write(self, tmpdir): From d5d4775c754b41ce52cecfad9abebfeb6db027d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Apr 2024 20:40:53 -0400 Subject: [PATCH 717/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/214.feature.1.rst | 1 - newsfragments/214.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/214.feature.1.rst delete mode 100644 newsfragments/214.feature.rst diff --git a/NEWS.rst b/NEWS.rst index f769bb47..eebc4ca2 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v16.14.0 +======== + +Features +-------- + +- Add .symlink_to and .hardlink_to. (#214) +- Add .cwd method and deprecated .getcwd. (#214) + + v16.13.0 ======== diff --git a/newsfragments/214.feature.1.rst b/newsfragments/214.feature.1.rst deleted file mode 100644 index 1cb5185c..00000000 --- a/newsfragments/214.feature.1.rst +++ /dev/null @@ -1 +0,0 @@ -Add .symlink_to and .hardlink_to. \ No newline at end of file diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst deleted file mode 100644 index 479c95d9..00000000 --- a/newsfragments/214.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add .cwd method and deprecated .getcwd. \ No newline at end of file From f84e1eeca24aaf53d147f29cbea0d190ecf75ad0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Apr 2024 14:21:46 -0400 Subject: [PATCH 718/835] Remove Python 2 compatibility with __div__ and __rdiv__. --- path/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 4520acae..c803459f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -207,8 +207,8 @@ def __radd__(self, other): return self._next_class(other.__add__(self)) # The / operator joins Paths. - def __div__(self, rel): - """fp.__div__(rel) == fp / rel == fp.joinpath(rel) + def __truediv__(self, rel): + """fp.__truediv__(rel) == fp / rel == fp.joinpath(rel) Join two path components, adding a separator character if needed. @@ -217,12 +217,9 @@ def __div__(self, rel): """ return self._next_class(self.module.join(self, rel)) - # Make the / operator work even when true division is enabled. - __truediv__ = __div__ - # The / operator joins Paths the other way around - def __rdiv__(self, rel): - """fp.__rdiv__(rel) == rel / fp + def __rtruediv__(self, rel): + """fp.__rtruediv__(rel) == rel / fp Join two path components, adding a separator character if needed. @@ -231,9 +228,6 @@ def __rdiv__(self, rel): """ return self._next_class(self.module.join(rel, self)) - # Make the / operator work even when true division is enabled. - __rtruediv__ = __rdiv__ - def __enter__(self): self._old_dir = self.cwd() os.chdir(self) From e02303c89d51c5130bd1f8e73fd30701e0c35797 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Apr 2024 14:43:23 -0400 Subject: [PATCH 719/835] Move the comment back to its target. --- path/masks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/masks.py b/path/masks.py index 13ae00a3..037ab5e8 100644 --- a/path/masks.py +++ b/path/masks.py @@ -4,10 +4,10 @@ import itertools -# from jaraco.functools from typing import Any, Callable +# from jaraco.functools def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa return functools.reduce(compose_two, funcs) From c2d6bc90d0c1dd3a4565eeeeccb77b1ec36e0b26 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Apr 2024 14:57:43 -0400 Subject: [PATCH 720/835] Replace typing overloads with a functools.wraps declaration. --- path/__init__.py | 99 +----------------------------------------------- 1 file changed, 1 insertion(+), 98 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index c803459f..9dcd2319 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -51,18 +51,8 @@ with contextlib.suppress(ImportError): import grp -from io import ( - BufferedRandom, - BufferedReader, - BufferedWriter, - FileIO, - TextIOWrapper, -) from typing import ( - IO, TYPE_CHECKING, - Any, - BinaryIO, Callable, Iterator, overload, @@ -71,12 +61,8 @@ if TYPE_CHECKING: from _typeshed import ( OpenBinaryMode, - OpenBinaryModeReading, - OpenBinaryModeUpdating, - OpenBinaryModeWriting, OpenTextMode, ) - from typing_extensions import Literal from . import classes, masks, matchers from .compat.py38 import removeprefix, removesuffix @@ -689,90 +675,7 @@ def iglob(self, pattern): # # --- Reading or writing an entire file at once. - @overload - def open( - self, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., - ) -> TextIOWrapper: ... - - @overload - def open( - self, - mode: OpenBinaryMode, - buffering: Literal[0], - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> FileIO: ... - - @overload - def open( - self, - mode: OpenBinaryModeUpdating, - buffering: Literal[-1, 1] = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedRandom: ... - - @overload - def open( - self, - mode: OpenBinaryModeReading, - buffering: Literal[-1, 1] = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedReader: ... - - @overload - def open( - self, - mode: OpenBinaryModeWriting, - buffering: Literal[-1, 1] = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BufferedWriter: ... - - @overload - def open( - self, - mode: OpenBinaryMode, - buffering: int, - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> BinaryIO: ... - - @overload - def open( - self, - mode: str, - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] = ..., - ) -> IO[Any]: ... - + @functools.wraps(open) def open(self, *args, **kwargs): """Open this file and return a corresponding file object. From 34ba6b2ec0650c8c70d9285a0c7ee1a126406807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 1 Apr 2024 17:47:04 +0200 Subject: [PATCH 721/835] Add link to blog entry from jaraco/skeleton#115 above CI build matrix. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a15c74a6..ac0ff69e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ env: jobs: test: strategy: + # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - "3.8" From 5f1582d090340de384a4c2371f4c918d73ebd5ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Apr 2024 17:10:21 -0400 Subject: [PATCH 722/835] Set assigned=() to avoid overwriting any properties of the wrapper. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 9dcd2319..0568d735 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -675,7 +675,7 @@ def iglob(self, pattern): # # --- Reading or writing an entire file at once. - @functools.wraps(open) + @functools.wraps(open, assigned=()) def open(self, *args, **kwargs): """Open this file and return a corresponding file object. From 7ad4f2fa9fb2b030d3ecc231fc24de181705622d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 16 Apr 2024 10:31:45 -0400 Subject: [PATCH 723/835] Pin against pytest 8.1.x due to pytest-dev/pytest#12194. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f46b6cbf..05ac4c76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = [options.extras_require] testing = # upstream - pytest >= 6, != 8.1.1 + pytest >= 6, != 8.1.* pytest-checkdocs >= 2.4 pytest-cov pytest-mypy From f4529af6a66e34d423860566be7882d665e10569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 16 Apr 2024 22:37:50 +0200 Subject: [PATCH 724/835] Move project metadata to `pyproject.toml` (jaraco/skeleton#122) Intentionally omitted specifying `tool.setuptools.include-package-data`: it's true by default in `pyproject.toml` according to https://setuptools.pypa.io/en/latest/userguide/datafiles.html#include-package-data. Closes jaraco/skeleton#121 --- pyproject.toml | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 42 ------------------------------------------ 2 files changed, 48 insertions(+), 43 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index a853c578..869fe7e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,52 @@ [build-system] -requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" +[project] +name = "PROJECT" +authors = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "PROJECT_DESCRIPTION" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] +requires-python = ">=3.8" +dependencies = [ +] +dynamic = ["version"] + +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6, != 8.1.*", + "pytest-checkdocs >= 2.4", + "pytest-cov", + "pytest-mypy", + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local +] +docs = [ + # upstream + "sphinx >= 3.5", + "jaraco.packaging >= 9.3", + "rst.linker >= 1.9", + "furo", + "sphinx-lint", + + # local +] + +[project.urls] +Homepage = "https://github.com/PROJECT_PATH" + +[project.scripts] + [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 05ac4c76..00000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[metadata] -name = PROJECT -author = Jason R. Coombs -author_email = jaraco@jaraco.com -description = PROJECT_DESCRIPTION -long_description = file:README.rst -url = https://github.com/PROJECT_PATH -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - -[options] -include_package_data = true -python_requires = >=3.8 -install_requires = - -[options.extras_require] -testing = - # upstream - pytest >= 6, != 8.1.* - pytest-checkdocs >= 2.4 - pytest-cov - pytest-mypy - pytest-enabler >= 2.2 - pytest-ruff >= 0.2.1 - - # local - -docs = - # upstream - sphinx >= 3.5 - jaraco.packaging >= 9.3 - rst.linker >= 1.9 - furo - sphinx-lint - - # local - -[options.entry_points] From 69b752a58f944254bcebed14b9182e6b744b4827 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Apr 2024 16:39:10 -0400 Subject: [PATCH 725/835] Migrated config to pyproject.toml using jaraco.develop.migrate-config and ini2toml. --- pyproject.toml | 60 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 56 ---------------------------------------------- 2 files changed, 59 insertions(+), 57 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index a853c578..e3688644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,63 @@ [build-system] -requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" +[project] +name = "path" +authors = [ + { name = "Jason Orendorff", email = "jason.orendorff@gmail.com" }, +] +maintainers = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "A module wrapper for os.path" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.8" +dependencies = [] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/jaraco/path" + +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6, != 8.1.1", + "pytest-checkdocs >= 2.4", + "pytest-cov", + "pytest-mypy", + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local + "appdirs", + "packaging", + 'pywin32; platform_system == "Windows" and python_version < "3.12"', + "more_itertools", + # required for checkdocs on README.rst + "pygments", +] +docs = [ + # upstream + "sphinx >= 3.5", + "jaraco.packaging >= 9.3", + "rst.linker >= 1.9", + "furo", + "sphinx-lint", + + # tidelift + "jaraco.tidelift >= 1.4", + + # local +] + [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 293e6b52..00000000 --- a/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -[metadata] -name = path -author = Jason Orendorff -author_email=jason.orendorff@gmail.com -maintainer = Jason R. Coombs -maintainer_email = jaraco@jaraco.com -description = A module wrapper for os.path -long_description = file:README.rst -url = https://github.com/jaraco/path -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Operating System :: OS Independent - Topic :: Software Development :: Libraries :: Python Modules - -[options] -include_package_data = true -python_requires = >=3.8 -install_requires = - -[options.extras_require] -testing = - # upstream - pytest >= 6, != 8.1.1 - pytest-checkdocs >= 2.4 - pytest-cov - pytest-mypy - pytest-enabler >= 2.2 - pytest-ruff >= 0.2.1 - - # local - appdirs - packaging - pywin32; platform_system == "Windows" and python_version < "3.12" - more_itertools - - # required for checkdocs on README.rst - pygments - -docs = - # upstream - sphinx >= 3.5 - jaraco.packaging >= 9.3 - rst.linker >= 1.9 - furo - sphinx-lint - - # tidelift - jaraco.tidelift >= 1.4 - - # local - -[options.entry_points] From 744cf2a2befb6a616657c105e5c9be9f3f921224 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2024 10:48:06 -0400 Subject: [PATCH 726/835] Allow macos on Python 3.8 to fail as GitHub CI has dropped support. Closes jaraco/skeleton#124. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69e..5ace4c50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }} steps: - uses: actions/checkout@v4 - name: Setup Python From bcf8f079eb729e7bcd50c10cf4da522620b00635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2024 11:06:12 -0400 Subject: [PATCH 727/835] Move project.urls to appear in the order that ini2toml generates it. Remove project.scripts. --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 869fe7e5..04b14cbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,9 @@ dependencies = [ ] dynamic = ["version"] +[project.urls] +Homepage = "https://github.com/PROJECT_PATH" + [project.optional-dependencies] testing = [ # upstream @@ -44,9 +47,4 @@ docs = [ # local ] -[project.urls] -Homepage = "https://github.com/PROJECT_PATH" - -[project.scripts] - [tool.setuptools_scm] From 67aab1554c7c9cbb19bb546a5b6476267030c5b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 2 May 2024 15:36:22 -0400 Subject: [PATCH 728/835] Revert "Allow macos on Python 3.8 to fail as GitHub CI has dropped support." This reverts commit 744cf2a2befb6a616657c105e5c9be9f3f921224. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ace4c50..ac0ff69e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - uses: actions/checkout@v4 - name: Setup Python From eaed9818e86b3be90418b77180ae3f49075ae035 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 21 May 2024 18:44:39 +0100 Subject: [PATCH 729/835] Add some missing type annotations --- path/__init__.pyi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/path/__init__.pyi b/path/__init__.pyi index 8f6805ff..d483765f 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -72,6 +72,9 @@ class Path(str): def expand(self: Self) -> Self: ... @property def stem(self) -> str: ... + def with_stem(self: Self, stem: str) -> Self: ... + @property + def suffix(self: Self) -> Self: ... @property def ext(self) -> str: ... def with_suffix(self: Self, suffix: str) -> Self: ... @@ -81,6 +84,7 @@ class Path(str): def parent(self: Self) -> Self: ... @property def name(self: Self) -> Self: ... + def with_name(self: Self, name: str) -> Self: ... def splitpath(self: Self) -> tuple[Self, str]: ... def splitdrive(self: Self) -> tuple[Self, Self]: ... def splitext(self: Self) -> tuple[Self, str]: ... From 1561bd433766808b0e55505d63979d7362d2a294 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Feb 2024 16:13:03 -0500 Subject: [PATCH 730/835] Registering project with tea. --- tea.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tea.yaml diff --git a/tea.yaml b/tea.yaml new file mode 100644 index 00000000..d7fadafa --- /dev/null +++ b/tea.yaml @@ -0,0 +1,6 @@ +# https://tea.xyz/what-is-this-file +--- +version: 1.0.0 +codeOwners: + - '0x32392EaEA1FDE87733bEEc3b184C9006501c4A82' +quorum: 1 From a595a0fad054cd20b69d3e954c99174e3a548938 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 31 May 2024 03:53:48 -0400 Subject: [PATCH 731/835] Rename extras to align with core metadata spec. Closes jaraco/skeleton#125. --- .readthedocs.yaml | 2 +- pyproject.toml | 4 ++-- tox.ini | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 85dfea9d..dc8516ac 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ python: install: - path: . extra_requirements: - - docs + - doc # required boilerplate readthedocs/readthedocs.org#10401 build: diff --git a/pyproject.toml b/pyproject.toml index 04b14cbc..50845ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dynamic = ["version"] Homepage = "https://github.com/PROJECT_PATH" [project.optional-dependencies] -testing = [ +test = [ # upstream "pytest >= 6, != 8.1.*", "pytest-checkdocs >= 2.4", @@ -36,7 +36,7 @@ testing = [ # local ] -docs = [ +doc = [ # upstream "sphinx >= 3.5", "jaraco.packaging >= 9.3", diff --git a/tox.ini b/tox.ini index 4c39a5b1..cc4db36e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ commands = pytest {posargs} usedevelop = True extras = - testing + test [testenv:diffcov] description = run tests and check that diff from main is covered @@ -22,8 +22,8 @@ commands = [testenv:docs] description = build the documentation extras = - docs - testing + doc + test changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html From 85cf1ffc1d2270b15d3a40fc8c73fa3f8e9a7127 Mon Sep 17 00:00:00 2001 From: ap Date: Tue, 11 Jun 2024 22:33:30 +0800 Subject: [PATCH 732/835] add missing type hint absolute() --- path/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/path/__init__.pyi b/path/__init__.pyi index 8f6805ff..28e82e3b 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -61,6 +61,7 @@ class Path(str): ) -> None: ... @classmethod def getcwd(cls: type[Self]) -> Self: ... + def absolute(self: Self) -> Self: ... def abspath(self: Self) -> Self: ... def normcase(self: Self) -> Self: ... def normpath(self: Self) -> Self: ... From c9729e1a0f66b7adad70c629518b7dab82ccd8c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Jun 2024 13:09:07 -0400 Subject: [PATCH 733/835] Prefer "Source" to "Homepage" for the repository label. Closes jaraco/skeleton#129 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50845ee3..ad67d3b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ dynamic = ["version"] [project.urls] -Homepage = "https://github.com/PROJECT_PATH" +Source = "https://github.com/PROJECT_PATH" [project.optional-dependencies] test = [ From 33c4896dbaeda2fd7a5fef701431dea05bb83bab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Jul 2024 15:34:09 -0400 Subject: [PATCH 734/835] Exclude pytest-ruff (and thus ruff), which cannot build on cygwin. Ref pypa/setuptools#3921 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad67d3b1..1307e1fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ test = [ "pytest-cov", "pytest-mypy", "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] From f087fb4ca05eb08c46abdd2cd67b18a3f33e3c79 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:32:46 +0200 Subject: [PATCH 735/835] "preserve" does not require preview any more (jaraco/skeleton#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * "preserve" does not require preview any more * Update URL in ruff.toml comment --------- Co-authored-by: Bartosz Sławecki --- ruff.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 70612985..7da4bee7 100644 --- a/ruff.toml +++ b/ruff.toml @@ -22,7 +22,5 @@ ignore = [ ] [format] -# Enable preview, required for quote-style = "preserve" -preview = true -# https://docs.astral.sh/ruff/settings/#format-quote-style +# https://docs.astral.sh/ruff/settings/#format_quote-style quote-style = "preserve" From 30f940e74b599400347d1162b7096f184cc46d31 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:34:53 +0200 Subject: [PATCH 736/835] Enforce ruff/Perflint rule PERF401 (jaraco/skeleton#132) --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index 7da4bee7..f1d03f83 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,7 @@ [lint] extend-select = [ "C901", + "PERF401", "W", ] ignore = [ From ab34814ca3ffe511ad63bb9589da06fd76758db8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 19 Jul 2024 12:33:01 -0400 Subject: [PATCH 737/835] Re-enable preview, this time not for one specific feature, but for all features in preview. Ref jaraco/skeleton#133 --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index f1d03f83..922aa1f1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -23,5 +23,8 @@ ignore = [ ] [format] +# Enable preview to get hugged parenthesis unwrapping and other nice surprises +# See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 +preview = true # https://docs.astral.sh/ruff/settings/#format_quote-style quote-style = "preserve" From 5e86abb47360dc0156c45621786c096f95785bca Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 19:28:40 -0400 Subject: [PATCH 738/835] Add news fragment. --- newsfragments/228.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/228.bugfix.rst diff --git a/newsfragments/228.bugfix.rst b/newsfragments/228.bugfix.rst new file mode 100644 index 00000000..0e534000 --- /dev/null +++ b/newsfragments/228.bugfix.rst @@ -0,0 +1 @@ +Add type hint for .absolute. \ No newline at end of file From d3cefca39e38063723a363cfa7e1a04ecd535606 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 19:29:56 -0400 Subject: [PATCH 739/835] Add news fragment. --- newsfragments/227.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/227.bugfix.rst diff --git a/newsfragments/227.bugfix.rst b/newsfragments/227.bugfix.rst new file mode 100644 index 00000000..3c4634bc --- /dev/null +++ b/newsfragments/227.bugfix.rst @@ -0,0 +1 @@ +Add type hints for .with_name, .suffix, .with_stem. \ No newline at end of file From 638a6b8ad1c3526ce76b37bbc1ec2141e8dc0f03 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 19:39:58 -0400 Subject: [PATCH 740/835] Add news fragment. --- newsfragments/225.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/225.feature.rst diff --git a/newsfragments/225.feature.rst b/newsfragments/225.feature.rst new file mode 100644 index 00000000..cb2c8150 --- /dev/null +++ b/newsfragments/225.feature.rst @@ -0,0 +1 @@ +Replaced 'open' overloads with 'functools.wraps(open)' for simple re-use. \ No newline at end of file From 73d82bfd8772e82b41378936495ebe2d7f15fefa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 20:03:18 -0400 Subject: [PATCH 741/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/compat/py38.py | 1 + 1 file changed, 1 insertion(+) diff --git a/path/compat/py38.py b/path/compat/py38.py index 0af38140..79ddc015 100644 --- a/path/compat/py38.py +++ b/path/compat/py38.py @@ -14,6 +14,7 @@ def removeprefix(self, prefix): return self[len(prefix) :] else: return self[:] + else: def removesuffix(self, suffix): From ade8831ef49ec7c54a4d2b1bff19b90bc0a5c0ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 20:53:08 -0400 Subject: [PATCH 742/835] Disable doctests on Python 3.11.9+ but not 3.12. Workaround for python/cpython#117692. Closes #231 --- conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..bdac1531 --- /dev/null +++ b/conftest.py @@ -0,0 +1,13 @@ +import sys + + +def pytest_configure(config): + disable_broken_doctests(config) + + +def disable_broken_doctests(config): + """ + Workaround for python/cpython#117692. + """ + if (3, 11, 9) <= sys.version_info < (3, 12): + config.option.doctestmodules = False From 28f8f74c03c44685162e00c31aa29e4fef199630 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Jul 2024 20:59:19 -0400 Subject: [PATCH 743/835] Finalize --- NEWS.rst | 16 ++++++++++++++++ newsfragments/225.feature.rst | 1 - newsfragments/227.bugfix.rst | 1 - newsfragments/228.bugfix.rst | 1 - 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/225.feature.rst delete mode 100644 newsfragments/227.bugfix.rst delete mode 100644 newsfragments/228.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index eebc4ca2..b53ed196 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,19 @@ +v16.15.0 +======== + +Features +-------- + +- Replaced 'open' overloads with 'functools.wraps(open)' for simple re-use. (#225) + + +Bugfixes +-------- + +- Add type hints for .with_name, .suffix, .with_stem. (#227) +- Add type hint for .absolute. (#228) + + v16.14.0 ======== diff --git a/newsfragments/225.feature.rst b/newsfragments/225.feature.rst deleted file mode 100644 index cb2c8150..00000000 --- a/newsfragments/225.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Replaced 'open' overloads with 'functools.wraps(open)' for simple re-use. \ No newline at end of file diff --git a/newsfragments/227.bugfix.rst b/newsfragments/227.bugfix.rst deleted file mode 100644 index 3c4634bc..00000000 --- a/newsfragments/227.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add type hints for .with_name, .suffix, .with_stem. \ No newline at end of file diff --git a/newsfragments/228.bugfix.rst b/newsfragments/228.bugfix.rst deleted file mode 100644 index 0e534000..00000000 --- a/newsfragments/228.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add type hint for .absolute. \ No newline at end of file From 66992c352bbfd65eca4ae196ac3e43714fe45a50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 05:03:27 -0400 Subject: [PATCH 744/835] Add .home classmethod. Ref #214 --- newsfragments/214.feature.rst | 1 + path/__init__.py | 4 ++++ test_path.py | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 newsfragments/214.feature.rst diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst new file mode 100644 index 00000000..6dd86410 --- /dev/null +++ b/newsfragments/214.feature.rst @@ -0,0 +1 @@ +Add .home classmethod. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index 0568d735..ab6e73f6 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -239,6 +239,10 @@ def getcwd(cls): ) return cls.cwd() + @classmethod + def home(cls) -> Path: + return cls(os.path.expanduser('~')) + # # --- Operations on Path strings. diff --git a/test_path.py b/test_path.py index b0caacd2..f1e92cc8 100644 --- a/test_path.py +++ b/test_path.py @@ -142,6 +142,11 @@ def test_cwd(self): assert isinstance(cwd, Path) assert cwd == os.getcwd() + def test_home(self): + home = Path.home() + assert isinstance(home, Path) + assert home == os.path.expanduser('~') + def test_explicit_module(self): """ The user may specify an explicit path module to use. From 3232088afa925950ab8400ad24c410f0959e88a3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 05:32:47 -0400 Subject: [PATCH 745/835] Implement .replace. Ref #214 --- newsfragments/214.feature.1.rst | 1 + path/__init__.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 newsfragments/214.feature.1.rst diff --git a/newsfragments/214.feature.1.rst b/newsfragments/214.feature.1.rst new file mode 100644 index 00000000..cb62775f --- /dev/null +++ b/newsfragments/214.feature.1.rst @@ -0,0 +1 @@ +Implement .replace. \ No newline at end of file diff --git a/path/__init__.py b/path/__init__.py index ab6e73f6..55532eda 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -35,6 +35,7 @@ import importlib import itertools import os +import pathlib import re import shutil import sys @@ -1310,6 +1311,40 @@ def renames(self, new): os.renames(self, new) return self._next_class(new) + def replace(self, target_or_old: Path | str, *args) -> Path: + """ + Replace a path or substitute substrings. + + Implements both pathlib.Path.replace and str.replace. + + If only a target is supplied, rename this path to the target path, + overwriting if that path exists. + + >>> dest = Path(getfixture('tmp_path')) + >>> orig = dest.joinpath('foo').touch() + >>> new = orig.replace(dest.joinpath('fee')) + >>> orig.exists() + False + >>> new.exists() + True + + ..seealso:: :meth:`pathlib.Path.replace` + + If a second parameter is supplied, perform a textual replacement. + + >>> Path('foo').replace('o', 'e') + Path('fee') + >>> Path('foo').replace('o', 'l', 1) + Path('flo') + + ..seealso:: :meth:`str.replace` + """ + return self._next_class( + super().replace(target_or_old, *args) + if args + else pathlib.Path(self).replace(target_or_old) + ) + # # --- Create/delete operations on directories From fb54581cc55d4122af11d01461001627835cdfb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 05:33:02 -0400 Subject: [PATCH 746/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/214.feature.1.rst | 1 - newsfragments/214.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/214.feature.1.rst delete mode 100644 newsfragments/214.feature.rst diff --git a/NEWS.rst b/NEWS.rst index b53ed196..034cea9d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v16.16.0 +======== + +Features +-------- + +- Implement .replace. (#214) +- Add .home classmethod. (#214) + + v16.15.0 ======== diff --git a/newsfragments/214.feature.1.rst b/newsfragments/214.feature.1.rst deleted file mode 100644 index cb62775f..00000000 --- a/newsfragments/214.feature.1.rst +++ /dev/null @@ -1 +0,0 @@ -Implement .replace. \ No newline at end of file diff --git a/newsfragments/214.feature.rst b/newsfragments/214.feature.rst deleted file mode 100644 index 6dd86410..00000000 --- a/newsfragments/214.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add .home classmethod. \ No newline at end of file From 7e23a1d7a5fc6412926e6e72be00e5479e86fec4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 06:30:29 -0400 Subject: [PATCH 747/835] Removed deprecated behaviors. --- newsfragments/+3aebd9d5.removal.rst | 1 + newsfragments/+c9d42aa7.removal.rst | 1 + path/__init__.py | 142 +++------------------------- path/__init__.pyi | 11 +-- test_path.py | 46 +-------- 5 files changed, 21 insertions(+), 180 deletions(-) create mode 100644 newsfragments/+3aebd9d5.removal.rst create mode 100644 newsfragments/+c9d42aa7.removal.rst diff --git a/newsfragments/+3aebd9d5.removal.rst b/newsfragments/+3aebd9d5.removal.rst new file mode 100644 index 00000000..2349ab5d --- /dev/null +++ b/newsfragments/+3aebd9d5.removal.rst @@ -0,0 +1 @@ +Removed deprecated support for passing ``bytes`` to ``write_text`` and ``write_lines(linesep=)`` parameter. diff --git a/newsfragments/+c9d42aa7.removal.rst b/newsfragments/+c9d42aa7.removal.rst new file mode 100644 index 00000000..e1a684a9 --- /dev/null +++ b/newsfragments/+c9d42aa7.removal.rst @@ -0,0 +1 @@ +Removed deprecated methods ``getcwd``, ``abspath``, ``ext``, ``listdir``, ``isdir``, ``isfile``, and ``text``. diff --git a/path/__init__.py b/path/__init__.py index 55532eda..fc002f83 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -231,15 +231,6 @@ def cwd(cls): """ return cls(os.getcwd()) - @classmethod - def getcwd(cls): - warnings.warn( - ".getcwd is deprecated; use cwd", - DeprecationWarning, - stacklevel=2, - ) - return cls.cwd() - @classmethod def home(cls) -> Path: return cls(os.path.expanduser('~')) @@ -251,14 +242,6 @@ def absolute(self): """.. seealso:: :func:`os.path.abspath`""" return self._next_class(self.module.abspath(self)) - def abspath(self): - warnings.warn( - ".abspath is deprecated; use absolute", - DeprecationWarning, - stacklevel=2, - ) - return self.absolute() - def normcase(self): """.. seealso:: :func:`os.path.normcase`""" return self._next_class(self.module.normcase(self)) @@ -320,15 +303,6 @@ def suffix(self): f, suffix = self.module.splitext(self) return suffix - @property - def ext(self): # pragma: no cover - warnings.warn( - ".ext is deprecated; use suffix", - DeprecationWarning, - stacklevel=2, - ) - return self.suffix - def with_suffix(self, suffix): """Return a new path with the file suffix changed (or added, if none) @@ -545,14 +519,6 @@ def iterdir(self, match=None): match = matchers.load(match) return filter(match, (self / child for child in os.listdir(self))) - def listdir(self, match=None): - warnings.warn( - ".listdir is deprecated; use iterdir", - DeprecationWarning, - stacklevel=2, - ) - return list(self.iterdir(match=match)) - def dirs(self, *args, **kwargs): """List of this directory's subdirectories. @@ -774,41 +740,14 @@ def read_bytes(self): with self.open(mode='rb') as f: return f.read() - def text(self, encoding=None, errors='strict'): - r"""Legacy function to read text. - - Converts all newline sequences to ``\n``. - """ - warnings.warn( - ".text is deprecated; use read_text", - DeprecationWarning, - stacklevel=2, - ) - return U_NEWLINE.sub('\n', self.read_text(encoding, errors)) - - @overload def write_text( self, text: str, - encoding: str | None = ..., - errors: str = ..., - linesep: str | None = ..., - append: bool = ..., - ) -> None: ... - - @overload - def write_text( - self, - text: builtins.bytes, - encoding: None = ..., - errors: str = ..., - linesep: str | None = ..., - append: bool = ..., - ) -> None: ... - - def write_text( - self, text, encoding=None, errors='strict', linesep=os.linesep, append=False - ): + encoding: str | None = None, + errors: str = 'strict', + linesep: str | None = os.linesep, + append: bool = False, + ) -> None: r"""Write the given text to this file. The default behavior is to overwrite any existing file; @@ -820,7 +759,7 @@ def write_text( Parameters: - `text` - str/bytes - The text to be written. + `text` - str - The text to be written. `encoding` - str - The text encoding used. @@ -855,29 +794,14 @@ def write_text( --- Unicode - If `text` isn't Unicode, then apart from newline handling, the - bytes are written verbatim to the file. The `encoding` and - `errors` arguments are not used and must be omitted. - - If `text` is Unicode, it is first converted to :func:`bytes` using the + `text` is written using the specified `encoding` (or the default encoding if `encoding` isn't specified). The `errors` argument applies only to this conversion. """ - if isinstance(text, str): - if linesep is not None: - text = U_NEWLINE.sub(linesep, text) - bytes = text.encode(encoding or sys.getdefaultencoding(), errors) - else: - warnings.warn( - "Writing bytes in write_text is deprecated", - DeprecationWarning, - stacklevel=1, - ) - assert encoding is None - if linesep is not None: - text = B_NEWLINE.sub(linesep.encode(), text) - bytes = text + if linesep is not None: + text = U_NEWLINE.sub(linesep, text) + bytes = text.encode(encoding or sys.getdefaultencoding(), errors) self.write_bytes(bytes, append=append) def lines(self, encoding=None, errors=None, retain=True): @@ -894,8 +818,6 @@ def lines(self, encoding=None, errors=None, retain=True): but translate all newline characters to ``\n``. If ``False``, newline characters are omitted. - - .. seealso:: :meth:`text` """ text = U_NEWLINE.sub('\n', self.read_text(encoding, errors)) return text.splitlines(retain) @@ -905,15 +827,14 @@ def write_lines( lines, encoding=None, errors='strict', - linesep=_default_linesep, + *, append=False, ): r"""Write the given lines of text to this file. By default this overwrites any existing file at this path. - This puts a platform-specific newline sequence on every line. - See `linesep` below. + Puts a platform-specific newline sequence on every line. `lines` - A list of strings. @@ -923,33 +844,16 @@ def write_lines( `errors` - How to handle errors in Unicode encoding. This also applies only to Unicode strings. - linesep - (deprecated) The desired line-ending. This line-ending is - applied to every line. If a line already has any - standard line ending (``'\r'``, ``'\n'``, ``'\r\n'``, - ``u'\x85'``, ``u'\r\x85'``, ``u'\u2028'``), that will - be stripped off and this will be used instead. The - default is os.linesep, which is platform-dependent - (``'\r\n'`` on Windows, ``'\n'`` on Unix, etc.). - Specify ``None`` to write the lines as-is, like - ``.writelines`` on a file object. - Use the keyword argument ``append=True`` to append lines to the file. The default is to overwrite the file. """ mode = 'a' if append else 'w' with self.open(mode, encoding=encoding, errors=errors, newline='') as f: - f.writelines(self._replace_linesep(lines, linesep)) + f.writelines(self._replace_linesep(lines)) @staticmethod - def _replace_linesep(lines, linesep): - if linesep != _default_linesep: - warnings.warn("linesep is deprecated", DeprecationWarning, stacklevel=3) - else: - linesep = os.linesep - if linesep is None: - return lines - - return (line + linesep for line in _strip_newlines(lines)) + def _replace_linesep(lines): + return (line + os.linesep for line in _strip_newlines(lines)) def read_md5(self): """Calculate the md5 hash for this file. @@ -1009,26 +913,10 @@ def exists(self): """.. seealso:: :func:`os.path.exists`""" return self.module.exists(self) - def isdir(self): # pragma: no cover - warnings.warn( - "isdir is deprecated; use is_dir", - DeprecationWarning, - stacklevel=2, - ) - return self.is_dir() - def is_dir(self): """.. seealso:: :func:`os.path.isdir`""" return self.module.isdir(self) - def isfile(self): # pragma: no cover - warnings.warn( - "isfile is deprecated; use is_file", - DeprecationWarning, - stacklevel=2, - ) - return self.is_file() - def is_file(self): """.. seealso:: :func:`os.path.isfile`""" return self.module.isfile(self) diff --git a/path/__init__.pyi b/path/__init__.pyi index 1cf626c3..2dba216e 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -59,10 +59,7 @@ class Path(str): exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... - @classmethod - def getcwd(cls: type[Self]) -> Self: ... def absolute(self: Self) -> Self: ... - def abspath(self: Self) -> Self: ... def normcase(self: Self) -> Self: ... def normpath(self: Self) -> Self: ... def realpath(self: Self) -> Self: ... @@ -76,8 +73,6 @@ class Path(str): def with_stem(self: Self, stem: str) -> Self: ... @property def suffix(self: Self) -> Self: ... - @property - def ext(self) -> str: ... def with_suffix(self: Self, suffix: str) -> Self: ... @property def drive(self: Self) -> Self: ... @@ -101,7 +96,6 @@ class Path(str): # --- Listing, searching, walking, and matching def iterdir(self: Self, match: _Match = ...) -> Iterator[Self]: ... - def listdir(self: Self, match: _Match = ...) -> list[Self]: ... def dirs(self: Self, match: _Match = ...) -> list[Self]: ... def files(self: Self, match: _Match = ...) -> list[Self]: ... def walk( @@ -132,7 +126,6 @@ class Path(str): self, encoding: str | None = ..., errors: str | None = ... ) -> str: ... def read_bytes(self) -> builtins.bytes: ... - def text(self, encoding: str | None = ..., errors: str = ...) -> str: ... def lines( self, encoding: str | None = ..., @@ -144,7 +137,7 @@ class Path(str): lines: list[str], encoding: str | None = ..., errors: str = ..., - linesep: str | None = ..., + *, append: bool = ..., ) -> None: ... def read_md5(self) -> builtins.bytes: ... @@ -152,9 +145,7 @@ class Path(str): def read_hexhash(self, hash_name: str) -> str: ... def isabs(self) -> bool: ... def exists(self) -> bool: ... - def isdir(self) -> bool: ... def is_dir(self) -> bool: ... - def isfile(self) -> bool: ... def is_file(self) -> bool: ... def islink(self) -> bool: ... def ismount(self) -> bool: ... diff --git a/test_path.py b/test_path.py index f1e92cc8..724dbeb8 100644 --- a/test_path.py +++ b/test_path.py @@ -318,11 +318,6 @@ def test_read_write(self, tmpdir): assert file.read_text(encoding='utf-8') == 'hello world' assert file.read_bytes() == b'hello world' - @pytest.mark.filterwarnings('ignore:Writing bytes in write_text') - def test_write_text_bytes(self, tmpdir): - file = path.Path(tmpdir) / 'filename' - file.write_text(b'hello world') - class TestPerformance: @staticmethod @@ -767,11 +762,9 @@ def test_unicode(self, tmpdir, encoding): with open(p, 'wb') as strm: strm.write(given.encode(encoding)) - # test all 3 path read-fully functions, including + # test read-fully functions, including # path.lines() in unicode mode. - assert p.bytes() == given.encode(encoding) - with pytest.deprecated_call(): - assert p.text(encoding) == clean + assert p.read_bytes() == given.encode(encoding) assert p.lines(encoding) == expectedLines assert p.lines(encoding, retain=False) == stripped @@ -793,8 +786,7 @@ def test_unicode(self, tmpdir, encoding): expectedLinesNoHanging = expectedLines[:] expectedLinesNoHanging[-1] += '\n' assert p.bytes() == expectedBytes - with pytest.deprecated_call(): - assert p.text(encoding) == 2 * cleanNoHanging + assert p.read_text(encoding) == 2 * cleanNoHanging assert p.lines(encoding) == 2 * expectedLinesNoHanging assert p.lines(encoding, retain=False) == 2 * stripped @@ -813,34 +805,6 @@ def test_unicode(self, tmpdir, encoding): # Check the result. assert p.bytes() == expectedBytes - # Same test, using newline sequences that are different - # from the platform default. - def testLinesep(eol): - p.write_lines(givenLines, encoding, linesep=eol) - p.write_lines(givenLines, encoding, linesep=eol, append=True) - expected = 2 * cleanNoHanging.replace('\n', eol).encode(encoding) - assert p.bytes() == expected - - with pytest.deprecated_call(): - testLinesep('\n') - testLinesep('\r') - testLinesep('\r\n') - testLinesep('\x0d\x85') - - # Again, but with linesep=None. - with pytest.deprecated_call(): - p.write_lines(givenLines, encoding, linesep=None) - p.write_lines(givenLines, encoding, linesep=None, append=True) - # Check the result. - expectedBytes = 2 * given.encode(encoding) - assert p.bytes() == expectedBytes - with pytest.deprecated_call(): - assert p.text(encoding) == 2 * clean - expectedResultLines = expectedLines[:] - expectedResultLines[-1] += expectedLines[0] - expectedResultLines += expectedLines[1:] - assert p.lines(encoding) == expectedResultLines - def test_chunks(self, tmpdir): p = (TempDir() / 'test.txt').touch() txt = "0123456789" @@ -1005,10 +969,6 @@ def test_nested(self): self.subdir_a.merge_tree(self.subdir_b) assert self.subdir_b.joinpath('subsub').is_dir() - def test_listdir(self): - with pytest.deprecated_call(): - Path().listdir() - class TestChdir: def test_chdir_or_cd(self, tmpdir): From d97218e74b1eba5ab8e3c18a77f7e16c3ec7be4f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 06:41:07 -0400 Subject: [PATCH 748/835] Remove crufty manifest.in --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index bb37a272..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include *.rst From 6b567d40b1fbacdddba159fcf83670cb50058d11 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 06:41:46 -0400 Subject: [PATCH 749/835] Remove crufty Dockerfile --- Dockerfile | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7b118564..00000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -from ubuntu:bionic -RUN apt update -RUN apt install -y python python-pip git -RUN python -m pip install tox -RUN mkdir /app -ENV LANG=C.UTF-8 -WORKDIR /app -COPY . . -CMD tox From e27e2bbe3f345ed9c654c7528993cdaf6de92cc2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 06:42:02 -0400 Subject: [PATCH 750/835] Move tests to tests directory. --- conftest.py => tests/conftest.py | 0 test_path.py => tests/test_path.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename conftest.py => tests/conftest.py (100%) rename test_path.py => tests/test_path.py (100%) diff --git a/conftest.py b/tests/conftest.py similarity index 100% rename from conftest.py rename to tests/conftest.py diff --git a/test_path.py b/tests/test_path.py similarity index 100% rename from test_path.py rename to tests/test_path.py From e03580edf6cfec719890599010e0b164d06af50f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 06:42:50 -0400 Subject: [PATCH 751/835] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+3aebd9d5.removal.rst | 1 - newsfragments/+c9d42aa7.removal.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+3aebd9d5.removal.rst delete mode 100644 newsfragments/+c9d42aa7.removal.rst diff --git a/NEWS.rst b/NEWS.rst index 034cea9d..93022d4f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v17.0.0 +======= + +Deprecations and Removals +------------------------- + +- Removed deprecated methods ``getcwd``, ``abspath``, ``ext``, ``listdir``, ``isdir``, ``isfile``, and ``text``. +- Removed deprecated support for passing ``bytes`` to ``write_text`` and ``write_lines(linesep=)`` parameter. + + v16.16.0 ======== diff --git a/newsfragments/+3aebd9d5.removal.rst b/newsfragments/+3aebd9d5.removal.rst deleted file mode 100644 index 2349ab5d..00000000 --- a/newsfragments/+3aebd9d5.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Removed deprecated support for passing ``bytes`` to ``write_text`` and ``write_lines(linesep=)`` parameter. diff --git a/newsfragments/+c9d42aa7.removal.rst b/newsfragments/+c9d42aa7.removal.rst deleted file mode 100644 index e1a684a9..00000000 --- a/newsfragments/+c9d42aa7.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Removed deprecated methods ``getcwd``, ``abspath``, ``ext``, ``listdir``, ``isdir``, ``isfile``, and ``text``. From 06acfd262258d809242c74179477af324389e1c7 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:14:35 +0200 Subject: [PATCH 752/835] Update to the latest ruff version (jaraco/skeleton#137) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a4a7e91..ff54405e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.5.6 hooks: - id: ruff - id: ruff-format From dd30b7600f33ce06a479a73002b950f4a3947759 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Aug 2024 17:19:17 -0400 Subject: [PATCH 753/835] Add Protocols, remove @overload, from `.coveragerc` `exclude_also` (jaraco/skeleton#135) Co-authored-by: Jason R. Coombs --- .coveragerc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 35b98b1d..2e3f4dd7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,8 @@ disable_warnings = [report] show_missing = True exclude_also = - # jaraco/skeleton#97 - @overload + # Exclude common false positives per + # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion + # Ref jaraco/skeleton#97 and jaraco/skeleton#135 + class .*\bProtocol\): if TYPE_CHECKING: From 3841656c61bad87f922fcba50445b503209b69c2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 12 Aug 2024 12:13:19 -0400 Subject: [PATCH 754/835] Loosen restrictions on mypy (jaraco/skeleton#136) Based on changes downstream in setuptools. --- mypy.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index b6f97276..83b0d15c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,14 @@ [mypy] -ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# Is the project well-typed? +strict = False + +# Early opt-in even when strict = False +warn_unused_ignores = True +warn_redundant_casts = True +enable_error_code = ignore-without-code + +# Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True + +# Disable overload-overlap due to many false-positives +disable_error_code = overload-overlap From 1a27fd5b8815e65571e6c028d6bef2c1daf61688 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Aug 2024 12:16:15 -0400 Subject: [PATCH 755/835] Split the test dependencies into four classes (test, cover, type, check). (jaraco/skeleton#139) --- pyproject.toml | 25 ++++++++++++++++++++----- tox.ini | 4 ++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1307e1fa..31057d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,10 @@ Source = "https://github.com/PROJECT_PATH" test = [ # upstream "pytest >= 6, != 8.1.*", - "pytest-checkdocs >= 2.4", - "pytest-cov", - "pytest-mypy", - "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] + doc = [ # upstream "sphinx >= 3.5", @@ -47,4 +43,23 @@ doc = [ # local ] +check = [ + "pytest-checkdocs >= 2.4", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", +] + +cover = [ + "pytest-cov", +] + +enabler = [ + "pytest-enabler >= 2.2", +] + +type = [ + "pytest-mypy", +] + + + [tool.setuptools_scm] diff --git a/tox.ini b/tox.ini index cc4db36e..01f0975f 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,10 @@ commands = usedevelop = True extras = test + check + cover + enabler + type [testenv:diffcov] description = run tests and check that diff from main is covered From f1350e413775a9e79e20779cc9705e28a1c55900 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 07:05:32 -0400 Subject: [PATCH 756/835] Add upstream and local sections for 'type' extra, since many projects will have 'types-*' dependencies. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 31057d85..3866a323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,10 @@ enabler = [ ] type = [ + # upstream "pytest-mypy", + + # local ] From 57b8aa81d77416805dcaaa22d5d45fef3e8b331c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 25 Aug 2024 09:29:10 +0100 Subject: [PATCH 757/835] Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes (jaraco/skeleton#140) * Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes. This is documented in https://github.com/astral-sh/ruff-pre-commit?tab=readme-ov-file#using-ruff-with-pre-commit and should be safe to apply, because it requires the developer to "manually approve" the suggested changes via `git add`. * Add --unsafe-fixes to ruff pre-commit hoot --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff54405e..8ec58e22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,4 +3,5 @@ repos: rev: v0.5.6 hooks: - id: ruff + args: [--fix, --unsafe-fixes] - id: ruff-format From d3e83beaec3bdf4a628f2f0ae0a52d21c84e346f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 06:33:23 -0400 Subject: [PATCH 758/835] Disable mypy for now. Ref jaraco/skeleton#143 --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3866a323..1d81b1cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,5 +64,8 @@ type = [ ] - [tool.setuptools_scm] + + +[tool.pytest-enabler.mypy] +# Disabled due to jaraco/skeleton#143 From 3fcabf10b810c8585b858fb81fc3cd8c5efe898d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 13:26:38 -0400 Subject: [PATCH 759/835] Move overload-overlap disablement to its own line for easier diffs and simpler relevant comments. Ref #142 --- mypy.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..2806c330 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,5 +10,6 @@ enable_error_code = ignore-without-code # Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True -# Disable overload-overlap due to many false-positives -disable_error_code = overload-overlap +disable_error_code = + # Disable due to many false positives + overload-overlap From 5b4accffa65497b2a4c1955ade560806f351a13d Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 27 Aug 2024 15:08:28 -0400 Subject: [PATCH 760/835] Pass mypy, update Self --- path/__init__.pyi | 140 +++++++++++++++++++++++----------------------- pyproject.toml | 4 -- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/path/__init__.pyi b/path/__init__.pyi index 2dba216e..d88187cc 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -15,9 +15,7 @@ from typing import ( Iterator, ) -from _typeshed import ( - Self, -) +from typing_extensions import Self from . import classes @@ -44,72 +42,72 @@ class Path(str): def using_module(cls, module: ModuleType) -> type[Path]: ... @classes.ClassProperty @classmethod - def _next_class(cls: type[Self]) -> type[Self]: ... + def _next_class(cls) -> type[Self]: ... def __repr__(self) -> str: ... - def __add__(self: Self, more: str) -> Self: ... - def __radd__(self: Self, other: str) -> Self: ... - def __div__(self: Self, rel: str) -> Self: ... - def __truediv__(self: Self, rel: str) -> Self: ... - def __rdiv__(self: Self, rel: str) -> Self: ... - def __rtruediv__(self: Self, rel: str) -> Self: ... - def __enter__(self: Self) -> Self: ... + def __add__(self, more: str) -> Self: ... + def __radd__(self, other: str) -> Self: ... + def __div__(self, rel: str) -> Self: ... + def __truediv__(self, rel: str) -> Self: ... + def __rdiv__(self, rel: str) -> Self: ... + def __rtruediv__(self, rel: str) -> Self: ... + def __enter__(self) -> Self: ... def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... - def absolute(self: Self) -> Self: ... - def normcase(self: Self) -> Self: ... - def normpath(self: Self) -> Self: ... - def realpath(self: Self) -> Self: ... - def expanduser(self: Self) -> Self: ... - def expandvars(self: Self) -> Self: ... - def dirname(self: Self) -> Self: ... - def basename(self: Self) -> Self: ... - def expand(self: Self) -> Self: ... + def absolute(self) -> Self: ... + def normcase(self) -> Self: ... + def normpath(self) -> Self: ... + def realpath(self) -> Self: ... + def expanduser(self) -> Self: ... + def expandvars(self) -> Self: ... + def dirname(self) -> Self: ... + def basename(self) -> Self: ... + def expand(self) -> Self: ... @property def stem(self) -> str: ... - def with_stem(self: Self, stem: str) -> Self: ... + def with_stem(self, stem: str) -> Self: ... @property - def suffix(self: Self) -> Self: ... - def with_suffix(self: Self, suffix: str) -> Self: ... + def suffix(self) -> Self: ... + def with_suffix(self, suffix: str) -> Self: ... @property - def drive(self: Self) -> Self: ... + def drive(self) -> Self: ... @property - def parent(self: Self) -> Self: ... + def parent(self) -> Self: ... @property - def name(self: Self) -> Self: ... - def with_name(self: Self, name: str) -> Self: ... - def splitpath(self: Self) -> tuple[Self, str]: ... - def splitdrive(self: Self) -> tuple[Self, Self]: ... - def splitext(self: Self) -> tuple[Self, str]: ... - def stripext(self: Self) -> Self: ... + def name(self) -> Self: ... + def with_name(self, name: str) -> Self: ... + def splitpath(self) -> tuple[Self, str]: ... + def splitdrive(self) -> tuple[Self, Self]: ... + def splitext(self) -> tuple[Self, str]: ... + def stripext(self) -> Self: ... @classes.multimethod - def joinpath(cls: Self, first: str, *others: str) -> Self: ... - def splitall(self: Self) -> list[Self | str]: ... - def parts(self: Self) -> tuple[Self | str, ...]: ... - def _parts(self: Self) -> Iterator[Self | str]: ... - def _parts_iter(self: Self) -> Iterator[Self | str]: ... - def relpath(self: Self, start: str = ...) -> Self: ... - def relpathto(self: Self, dest: str) -> Self: ... + def joinpath(cls, first: str, *others: str) -> Self: ... + def splitall(self) -> list[Self | str]: ... + def parts(self) -> tuple[Self | str, ...]: ... + def _parts(self) -> Iterator[Self | str]: ... + def _parts_iter(self) -> Iterator[Self | str]: ... + def relpath(self, start: str = ...) -> Self: ... + def relpathto(self, dest: str) -> Self: ... # --- Listing, searching, walking, and matching - def iterdir(self: Self, match: _Match = ...) -> Iterator[Self]: ... - def dirs(self: Self, match: _Match = ...) -> list[Self]: ... - def files(self: Self, match: _Match = ...) -> list[Self]: ... + def iterdir(self, match: _Match = ...) -> Iterator[Self]: ... + def dirs(self, match: _Match = ...) -> list[Self]: ... + def files(self, match: _Match = ...) -> list[Self]: ... def walk( - self: Self, + self, match: _Match = ..., errors: str = ..., ) -> Generator[Self, Callable[[], bool] | None, None]: ... def walkdirs( - self: Self, + self, match: _Match = ..., errors: str = ..., ) -> Iterator[Self]: ... def walkfiles( - self: Self, + self, match: _Match = ..., errors: str = ..., ) -> Iterator[Self]: ... @@ -118,8 +116,8 @@ class Path(str): pattern: Path | str, normcase: Callable[[str], str] | None = ..., ) -> bool: ... - def glob(self: Self, pattern: str) -> list[Self]: ... - def iglob(self: Self, pattern: str) -> Iterator[Self]: ... + def glob(self, pattern: str) -> list[Self]: ... + def iglob(self, pattern: str) -> Iterator[Self]: ... def bytes(self) -> builtins.bytes: ... def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... def read_text( @@ -188,30 +186,30 @@ class Path(str): dir_fd: int | None = ..., follow_symlinks: bool = ..., ) -> Path: ... - def chmod(self: Self, mode: str | int) -> Self: ... + def chmod(self, mode: str | int) -> Self: ... if sys.platform != 'win32': - def chown(self: Self, uid: int | str = ..., gid: int | str = ...) -> Self: ... + def chown(self, uid: int | str = ..., gid: int | str = ...) -> Self: ... - def rename(self: Self, new: str) -> Self: ... - def renames(self: Self, new: str) -> Self: ... - def mkdir(self: Self, mode: int = ...) -> Self: ... - def mkdir_p(self: Self, mode: int = ...) -> Self: ... - def makedirs(self: Self, mode: int = ...) -> Self: ... - def makedirs_p(self: Self, mode: int = ...) -> Self: ... - def rmdir(self: Self) -> Self: ... - def rmdir_p(self: Self) -> Self: ... - def removedirs(self: Self) -> Self: ... - def removedirs_p(self: Self) -> Self: ... - def touch(self: Self) -> Self: ... - def remove(self: Self) -> Self: ... - def remove_p(self: Self) -> Self: ... - def unlink(self: Self) -> Self: ... - def unlink_p(self: Self) -> Self: ... - def link(self: Self, newpath: str) -> Self: ... - def symlink(self: Self, newlink: str | None = ...) -> Self: ... - def readlink(self: Self) -> Self: ... - def readlinkabs(self: Self) -> Self: ... + def rename(self, new: str) -> Self: ... + def renames(self, new: str) -> Self: ... + def mkdir(self, mode: int = ...) -> Self: ... + def mkdir_p(self, mode: int = ...) -> Self: ... + def makedirs(self, mode: int = ...) -> Self: ... + def makedirs_p(self, mode: int = ...) -> Self: ... + def rmdir(self) -> Self: ... + def rmdir_p(self) -> Self: ... + def removedirs(self) -> Self: ... + def removedirs_p(self) -> Self: ... + def touch(self) -> Self: ... + def remove(self) -> Self: ... + def remove_p(self) -> Self: ... + def unlink(self) -> Self: ... + def unlink_p(self) -> Self: ... + def link(self, newpath: str) -> Self: ... + def symlink(self, newlink: str | None = ...) -> Self: ... + def readlink(self) -> Self: ... + def readlinkabs(self) -> Self: ... def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... @@ -234,7 +232,7 @@ class Path(str): ignore_errors: bool = ..., onerror: Callable[[Any, Any, Any], Any] | None = ..., ) -> None: ... - def rmtree_p(self: Self) -> Self: ... + def rmtree_p(self) -> Self: ... def chdir(self) -> None: ... def cd(self) -> None: ... def merge_tree( @@ -250,7 +248,7 @@ class Path(str): def chroot(self) -> None: ... if sys.platform == 'win32': - def startfile(self: Self, operation: str | None = ...) -> Self: ... + def startfile(self, operation: str | None = ...) -> Self: ... @contextlib.contextmanager def in_place( @@ -313,13 +311,13 @@ class TempDir(Path): @classmethod def _next_class(cls) -> type[Path]: ... def __new__( - cls: type[Self], + cls, suffix: AnyStr | None = ..., prefix: AnyStr | None = ..., dir: AnyStr | os.PathLike[AnyStr] | None = ..., ) -> Self: ... def __init__(self) -> None: ... - def __enter__(self) -> Path: ... # type: ignore + def __enter__(self) -> Self: ... def __exit__( self, exc_type: type[BaseException] | None, diff --git a/pyproject.toml b/pyproject.toml index 044bfa00..43e18a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 From 0c326f3f77b2420163f73d97f8fbd090fa49147d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 13:13:06 -0400 Subject: [PATCH 761/835] Add a degenerate nitpick_ignore for downstream consumers. Add a 'local' comment to delineate where the skeleton ends and the downstream begins. --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 32150488..3d956a8c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', @@ -30,6 +33,7 @@ # Be strict about any broken references nitpicky = True +nitpick_ignore: list[tuple[str, str]] = [] # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 @@ -40,3 +44,5 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True + +# local From 2beb8b0c9d0f7046370e7c58c4e6baaf35154a16 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:26:28 -0400 Subject: [PATCH 762/835] Add support for linking usernames. Closes jaraco/skeleton#144 --- docs/conf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3d956a8c..d5745d62 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,4 +45,13 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True +# Add support for linking usernames, PyPI projects, Wikipedia pages +github_url = 'https://github.com/' +extlinks = { + 'user': (f'{github_url}%s', '@%s'), + 'pypi': ('https://pypi.org/project/%s', '%s'), + 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), +} +extensions += ['sphinx.ext.extlinks'] + # local From 790fa6e6feb9a93d39135494819b12e9df8a7bba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:53:52 -0400 Subject: [PATCH 763/835] Include the trailing slash in disable_error_code(overload-overlap), also required for clean diffs. Ref jaraco/skeleton#142 --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 2806c330..efcb8cbc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,4 +12,4 @@ explicit_package_bases = True disable_error_code = # Disable due to many false positives - overload-overlap + overload-overlap, From 1a6e38c0bfccd18a01deaca1491bcde3e778404c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 31 Aug 2024 05:50:38 -0400 Subject: [PATCH 764/835] Remove workaround for sphinx-contrib/sphinx-lint#83 --- tox.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 01f0975f..14243051 100644 --- a/tox.ini +++ b/tox.ini @@ -31,9 +31,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint \ - # workaround for sphinx-contrib/sphinx-lint#83 - --jobs 1 + python -m sphinxlint [testenv:finalize] description = assemble changelog and tag a release From a675458e1a7d6ae81d0d441338a74dc98ffc5a61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Sep 2024 10:16:01 -0400 Subject: [PATCH 765/835] Allow the workflow to be triggered manually. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69e..441b93ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ on: # required if branches-ignore is supplied (jaraco/skeleton#103) - '**' pull_request: + workflow_dispatch: permissions: contents: read From 81b766c06cc83679c4a04c2bfa6d2c8cc559bf33 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 11 Sep 2024 18:14:38 -0400 Subject: [PATCH 766/835] Fix an incompatibility (and source of merge conflicts) with projects using Ruff/isort. Remove extra line after imports in conf.py (jaraco/skeleton#147) --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d5745d62..240329c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From 3fe8c5ba792fd58a5a24eef4e8a845f3b5dd6c2c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 18:14:58 -0400 Subject: [PATCH 767/835] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#146) --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 441b93ef..251b9c1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: matrix: python: - "3.8" - - "3.12" + - "3.13" platform: - ubuntu-latest - macos-latest @@ -48,10 +48,14 @@ jobs: platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest + - python: "3.12" + platform: ubuntu-latest + - python: "3.14" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 - name: Setup Python From 62b6678a32087ed3bfc8ff19761764340295834e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 26 Oct 2024 00:12:59 +0100 Subject: [PATCH 768/835] Bump pre-commit hook for ruff to avoid clashes with pytest-ruff (jaraco/skeleton#150) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ec58e22..04870d16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.7.1 hooks: - id: ruff args: [--fix, --unsafe-fixes] From db4dfc495552aca8d6f05ed58441fa65fdc2ed9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 09:11:52 -0700 Subject: [PATCH 769/835] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#151) From e61a9df7cdc9c8d1b56c30b7b3f94a7cdac14414 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:19:31 -0400 Subject: [PATCH 770/835] Include pyproject.toml in ruff.toml. Closes jaraco/skeleton#119. Workaround for astral-sh/ruff#10299. --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index 922aa1f1..8b22940a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,6 @@ +# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +include = "pyproject.toml" + [lint] extend-select = [ "C901", From 750a1891ec4a1c0602050e3463e9593a8c13aa14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:22:50 -0400 Subject: [PATCH 771/835] Require Python 3.9 or later now that Python 3.8 is EOL. --- .github/workflows/main.yml | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 251b9c1d..9c01fc4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,15 +35,13 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.8" + - "3.9" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.9" - platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: "3.11" diff --git a/pyproject.toml b/pyproject.toml index 1d81b1cc..328b98cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ ] dynamic = ["version"] From 5c34e69568f23a524af4fa9dad3f5e80f22ec3e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Nov 2024 22:35:31 -0400 Subject: [PATCH 772/835] Use extend for proper workaround. Closes jaraco/skeleton#152 Workaround for astral-sh/ruff#10299 --- ruff.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index 8b22940a..9379d6e1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ -# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -include = "pyproject.toml" +# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +extend = "pyproject.toml" [lint] extend-select = [ From 049e49551896f1846c2eb535579118bf70b5aaee Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 13:57:22 -0800 Subject: [PATCH 773/835] Make annotations in-source for classes.py Remove classes.pyi. --- path/classes.py | 10 +++++++--- path/classes.pyi | 8 -------- 2 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 path/classes.pyi diff --git a/path/classes.py b/path/classes.py index b6101d0a..2914996b 100644 --- a/path/classes.py +++ b/path/classes.py @@ -1,8 +1,10 @@ import functools +from typing import Any, Callable class ClassProperty(property): - def __get__(self, cls, owner): + def __get__(self, cls: Any, owner: type | None = None) -> Any: + assert self.fget is not None return self.fget.__get__(None, owner)() @@ -12,10 +14,12 @@ class multimethod: instancemethod when invoked from the instance. """ - def __init__(self, func): + func: Callable[..., Any] + + def __init__(self, func: Callable[..., Any]): self.func = func - def __get__(self, instance, owner): + def __get__(self, instance: Any | None, owner: type | None) -> Any: """ If called on an instance, pass the instance as the first argument. diff --git a/path/classes.pyi b/path/classes.pyi deleted file mode 100644 index 0e119d0b..00000000 --- a/path/classes.pyi +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Any, Callable, Optional - -class ClassProperty(property): - def __get__(self, cls: Any, owner: type | None = ...) -> Any: ... - -class multimethod: - def __init__(self, func: Callable[..., Any]): ... - def __get__(self, instance: Any, owner: type | None) -> Any: ... From c544d87662204b484b8fb49475276fe926e81feb Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 14:09:30 -0800 Subject: [PATCH 774/835] Make annotations in-source for matchers.py Remove matchers.pyi. --- path/matchers.py | 17 ++++++++++++----- path/matchers.pyi | 21 --------------------- 2 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 path/matchers.pyi diff --git a/path/matchers.py b/path/matchers.py index 20ca92e2..7ddc8772 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -2,7 +2,10 @@ import fnmatch import ntpath -from typing import Any, overload +from typing import TYPE_CHECKING, Any, Callable, overload + +if TYPE_CHECKING: + from typing_extensions import Literal @overload @@ -36,15 +39,18 @@ class Base: class Null(Base): - def __call__(self, path): + def __call__(self, path: str) -> Literal[True]: return True class Pattern(Base): - def __init__(self, pattern): + pattern: str + _pattern: str + + def __init__(self, pattern: str): self.pattern = pattern - def get_pattern(self, normcase): + def get_pattern(self, normcase: Callable[[str], str]) -> str: try: return self._pattern except AttributeError: @@ -52,7 +58,8 @@ def get_pattern(self, normcase): self._pattern = normcase(self.pattern) return self._pattern - def __call__(self, path): + # NOTE: 'path' should be annotated with Path, but cannot due to circular imports. + def __call__(self, path) -> bool: normcase = getattr(self, 'normcase', path.module.normcase) pattern = self.get_pattern(normcase) return fnmatch.fnmatchcase(normcase(path.name), pattern) diff --git a/path/matchers.pyi b/path/matchers.pyi deleted file mode 100644 index 4c4925d3..00000000 --- a/path/matchers.pyi +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -from typing import Callable - -from typing_extensions import Literal - -from path import Path - -class Base: - pass - -class Null(Base): - def __call__(self, path: str) -> Literal[True]: ... - -class Pattern(Base): - def __init__(self, pattern: str) -> None: ... - def get_pattern(self, normcase: Callable[[str], str]) -> str: ... - def __call__(self, path: Path) -> bool: ... - -class CaseInsensitive(Pattern): - normcase: Callable[[str], str] From ee77cb39efa60e309dddb3fd31f41b1a5f6c5097 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 14:25:05 -0800 Subject: [PATCH 775/835] Add annotations to masks.py --- path/masks.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/path/masks.py b/path/masks.py index c7de97ca..2c727bee 100644 --- a/path/masks.py +++ b/path/masks.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import functools import itertools import operator import re -from typing import Any, Callable +from typing import Any, Callable, Iterable, Iterator # from jaraco.functools @@ -13,7 +15,7 @@ def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: # from jaraco.structures.binary -def gen_bit_values(number): +def gen_bit_values(number: int) -> Iterator[int]: """ Return a zero or one for each bit of a numeric value up to the most significant 1 bit, beginning with the least significant bit. @@ -26,7 +28,12 @@ def gen_bit_values(number): # from more_itertools -def padded(iterable, fillvalue=None, n=None, next_multiple=False): +def padded( + iterable: Iterable[Any], + fillvalue: Any | None = None, + n: int | None = None, + next_multiple: bool = False, +) -> Iterator[Any]: """Yield the elements from *iterable*, followed by *fillvalue*, such that at least *n* items are emitted. @@ -148,14 +155,14 @@ class Permissions(int): """ @property - def symbolic(self): + def symbolic(self) -> str: return ''.join( ['-', val][bit] for val, bit in zip(itertools.cycle('rwx'), self.bits) ) @property - def bits(self): + def bits(self) -> Iterator[int]: return reversed(tuple(padded(gen_bit_values(self), 0, n=9))) - def __str__(self): + def __str__(self) -> str: return self.symbolic From e0be2fc6c0f48da874d5501f57d1b179798dc6a4 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 19:43:58 -0800 Subject: [PATCH 776/835] Copy annotations from __init__.pyi to __init__.py __init__.pyi has been removed. Annotations were copied from the stuf file as-is, without changing either the annotations or anything about the source code (with exceptions mentioned below). In some cases, the annotations are not correct (or better ones could be made) or mypy will now flag code inside the annotated methods - these will be corrected in subsequent commits. This commit is intended to serve as a reference point for translation from stub annotations to in-source annotations. The exceptions to as-is copy are the following: - When the annotation for an input parameter was "Path | str" it has been replaced with "str" since "Path" is a subclass of "str" - When the return type was "Path" it has been replaced with "Self" (except in the cases where it truely should be "Path" like in "TempDir") - In cases where the stub contained explicit parameters and the source had "*args, **kwargs", these have been replaced with explicit parameters to match the stub. In several of these cases, this required changing the source code to call the forwarded function with the explict parameters. --- path/__init__.py | 374 +++++++++++++++++++++++++++------------------- path/__init__.pyi | 330 ---------------------------------------- 2 files changed, 220 insertions(+), 484 deletions(-) delete mode 100644 path/__init__.pyi diff --git a/path/__init__.py b/path/__init__.py index fc002f83..de7c2aa3 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -42,6 +42,7 @@ import tempfile import warnings from numbers import Number +from types import ModuleType, TracebackType with contextlib.suppress(ImportError): import win32security @@ -54,12 +55,17 @@ from typing import ( TYPE_CHECKING, + IO, + Any, Callable, + Generator, + Iterable, Iterator, overload, ) if TYPE_CHECKING: + from typing_extensions import Never, Self from _typeshed import ( OpenBinaryMode, OpenTextMode, @@ -70,6 +76,8 @@ __all__ = ['Path', 'TempDir'] +# Type for the match argument for several methods +_Match = str | Callable[[str], bool] | None LINESEPS = ['\r\n', '\r', '\n'] U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] @@ -111,10 +119,12 @@ class Traversal: False """ - def __init__(self, follow): + def __init__(self, follow: Callable[[Path], bool]): self.follow = follow - def __call__(self, walker): + def __call__( + self, walker: Generator[Path, Callable[[], bool] | None, None] + ) -> Iterator[Path]: traverse = None while True: try: @@ -126,7 +136,7 @@ def __call__(self, walker): traverse = functools.partial(self.follow, item) -def _strip_newlines(lines): +def _strip_newlines(lines: Iterable[str]) -> Iterator[str]: r""" >>> list(_strip_newlines(['Hello World\r\n', 'foo'])) ['Hello World', 'foo'] @@ -150,7 +160,7 @@ class Path(str): the Path instance. """ - module = os.path + module: Any = os.path """ The path module to use for path operations. .. seealso:: :mod:`os.path` @@ -159,7 +169,7 @@ class Path(str): def __new__(cls, other='.'): return super().__new__(cls, other) - def __init__(self, other='.'): + def __init__(self, other: Any = '.') -> None: if other is None: raise TypeError("Invalid initial value for path: None") with contextlib.suppress(AttributeError): @@ -167,7 +177,7 @@ def __init__(self, other='.'): @classmethod @functools.lru_cache - def using_module(cls, module): + def using_module(cls, module: ModuleType) -> type[Self]: subclass_name = cls.__name__ + '_' + module.__name__ bases = (cls,) ns = {'module': module} @@ -175,7 +185,7 @@ def using_module(cls, module): @classes.ClassProperty @classmethod - def _next_class(cls): + def _next_class(cls) -> type[Self]: """ What class should be used to construct new instances from this class """ @@ -183,18 +193,18 @@ def _next_class(cls): # --- Special Python methods. - def __repr__(self): + def __repr__(self) -> str: return f'{type(self).__name__}({super().__repr__()})' # Adding a Path and a string yields a Path. - def __add__(self, more): + def __add__(self, more: str) -> Self: return self._next_class(super().__add__(more)) - def __radd__(self, other): + def __radd__(self, other: str) -> Self: return self._next_class(other.__add__(self)) # The / operator joins Paths. - def __truediv__(self, rel): + def __truediv__(self, rel: str) -> Self: """fp.__truediv__(rel) == fp / rel == fp.joinpath(rel) Join two path components, adding a separator character if @@ -205,7 +215,7 @@ def __truediv__(self, rel): return self._next_class(self.module.join(self, rel)) # The / operator joins Paths the other way around - def __rtruediv__(self, rel): + def __rtruediv__(self, rel: str) -> Self: """fp.__rtruediv__(rel) == rel / fp Join two path components, adding a separator character if @@ -215,12 +225,17 @@ def __rtruediv__(self, rel): """ return self._next_class(self.module.join(rel, self)) - def __enter__(self): + def __enter__(self) -> Self: self._old_dir = self.cwd() os.chdir(self) return self - def __exit__(self, *_): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: os.chdir(self._old_dir) @classmethod @@ -238,39 +253,39 @@ def home(cls) -> Path: # # --- Operations on Path strings. - def absolute(self): + def absolute(self) -> Self: """.. seealso:: :func:`os.path.abspath`""" return self._next_class(self.module.abspath(self)) - def normcase(self): + def normcase(self) -> Self: """.. seealso:: :func:`os.path.normcase`""" return self._next_class(self.module.normcase(self)) - def normpath(self): + def normpath(self) -> Self: """.. seealso:: :func:`os.path.normpath`""" return self._next_class(self.module.normpath(self)) - def realpath(self): + def realpath(self) -> Self: """.. seealso:: :func:`os.path.realpath`""" return self._next_class(self.module.realpath(self)) - def expanduser(self): + def expanduser(self) -> Self: """.. seealso:: :func:`os.path.expanduser`""" return self._next_class(self.module.expanduser(self)) - def expandvars(self): + def expandvars(self) -> Self: """.. seealso:: :func:`os.path.expandvars`""" return self._next_class(self.module.expandvars(self)) - def dirname(self): + def dirname(self) -> Self: """.. seealso:: :attr:`parent`, :func:`os.path.dirname`""" return self._next_class(self.module.dirname(self)) - def basename(self): + def basename(self) -> Self: """.. seealso:: :attr:`name`, :func:`os.path.basename`""" return self._next_class(self.module.basename(self)) - def expand(self): + def expand(self) -> Self: """Clean up a filename by calling :meth:`expandvars()`, :meth:`expanduser()`, and :meth:`normpath()` on it. @@ -280,7 +295,7 @@ def expand(self): return self.expandvars().expanduser().normpath() @property - def stem(self): + def stem(self) -> str: """The same as :meth:`name`, but with one file extension stripped off. >>> Path('/home/guido/python.tar.gz').stem @@ -289,7 +304,7 @@ def stem(self): base, ext = self.module.splitext(self.name) return base - def with_stem(self, stem): + def with_stem(self, stem: str) -> Self: """Return a new path with the stem changed. >>> Path('/home/guido/python.tar.gz').with_stem("foo") @@ -298,12 +313,12 @@ def with_stem(self, stem): return self.with_name(stem + self.suffix) @property - def suffix(self): + def suffix(self) -> Self: """The file extension, for example ``'.py'``.""" f, suffix = self.module.splitext(self) return suffix - def with_suffix(self, suffix): + def with_suffix(self, suffix: str) -> Self: """Return a new path with the file suffix changed (or added, if none) >>> Path('/home/guido/python.tar.gz').with_suffix(".foo") @@ -323,7 +338,7 @@ def with_suffix(self, suffix): return self.stripext() + suffix @property - def drive(self): + def drive(self) -> Self: """The drive specifier, for example ``'C:'``. This is always empty on systems that don't use drive specifiers. @@ -358,7 +373,7 @@ def drive(self): """, ) - def with_name(self, name): + def with_name(self, name: str) -> Self: """Return a new path with the name changed. >>> Path('/home/guido/python.tar.gz').with_name("foo.zip") @@ -366,7 +381,7 @@ def with_name(self, name): """ return self._next_class(removesuffix(self, self.name) + name) - def splitpath(self): + def splitpath(self) -> tuple[Self, str]: """Return two-tuple of ``.parent``, ``.name``. .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split` @@ -374,7 +389,7 @@ def splitpath(self): parent, child = self.module.split(self) return self._next_class(parent), child - def splitdrive(self): + def splitdrive(self) -> tuple[Self, Self]: """Return two-tuple of ``.drive`` and rest without drive. Split the drive specifier from this path. If there is @@ -386,7 +401,7 @@ def splitdrive(self): drive, rel = self.module.splitdrive(self) return self._next_class(drive), self._next_class(rel) - def splitext(self): + def splitext(self) -> tuple[Self, str]: """Return two-tuple of ``.stripext()`` and ``.ext``. Split the filename extension from this path and return @@ -401,7 +416,7 @@ def splitext(self): filename, ext = self.module.splitext(self) return self._next_class(filename), ext - def stripext(self): + def stripext(self) -> Self: """Remove one file extension from the path. For example, ``Path('/home/guido/python.tar.gz').stripext()`` @@ -410,7 +425,7 @@ def stripext(self): return self.splitext()[0] @classes.multimethod - def joinpath(cls, first, *others): + def joinpath(cls, first: str, *others: str) -> Self: """ Join first to zero or more :class:`Path` components, adding a separator character (:samp:`{first}.module.sep`) @@ -421,7 +436,7 @@ def joinpath(cls, first, *others): """ return cls._next_class(cls.module.join(first, *others)) - def splitall(self): + def splitall(self) -> list[Self | str]: r"""Return a list of the path components in this path. The first item in the list will be a Path. Its value will be @@ -436,17 +451,17 @@ def splitall(self): """ return list(self._parts()) - def parts(self): + def parts(self) -> tuple[Self | str]: """ >>> Path('/foo/bar/baz').parts() (Path('/'), 'foo', 'bar', 'baz') """ return tuple(self._parts()) - def _parts(self): + def _parts(self) -> Iterator[Self | str]: return reversed(tuple(self._parts_iter())) - def _parts_iter(self): + def _parts_iter(self) -> Iterator[Self | str]: loc = self while loc != os.curdir and loc != os.pardir: prev = loc @@ -456,14 +471,14 @@ def _parts_iter(self): yield child yield loc - def relpath(self, start='.'): + def relpath(self, start: str = '.') -> Self: """Return this path as a relative path, based from `start`, which defaults to the current working directory. """ cwd = self._next_class(start) return cwd.relpathto(self) - def relpathto(self, dest): + def relpathto(self, dest: str) -> Self: """Return a relative path from `self` to `dest`. If there is no relative path from `self` to `dest`, for example if @@ -503,7 +518,7 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching - def iterdir(self, match=None): + def iterdir(self, match: _Match = None) -> Iterator[Self]: """Yields items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing @@ -519,7 +534,7 @@ def iterdir(self, match=None): match = matchers.load(match) return filter(match, (self / child for child in os.listdir(self))) - def dirs(self, *args, **kwargs): + def dirs(self, match: _Match = None) -> list[Self]: """List of this directory's subdirectories. The elements of the list are Path objects. @@ -528,9 +543,9 @@ def dirs(self, *args, **kwargs): Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.iterdir(*args, **kwargs) if p.is_dir()] + return [p for p in self.iterdir(match) if p.is_dir()] - def files(self, *args, **kwargs): + def files(self, match: _Match = None) -> list[Self]: """List of the files in self. The elements of the list are Path objects. @@ -539,9 +554,11 @@ def files(self, *args, **kwargs): Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.iterdir(*args, **kwargs) if p.is_file()] + return [p for p in self.iterdir(match) if p.is_file()] - def walk(self, match=None, errors='strict'): + def walk( + self, match: _Match = None, errors: str = 'strict' + ) -> Generator[Self, Callable[[], bool] | None, None]: """Iterator over files and subdirs, recursively. The iterator yields Path objects naming each child item of @@ -581,15 +598,17 @@ def walk(self, match=None, errors='strict'): if do_traverse: yield from child.walk(errors=errors, match=match) - def walkdirs(self, *args, **kwargs): + def walkdirs(self, match: _Match = None, errors: str = 'strict') -> Iterator[Self]: """Iterator over subdirs, recursively.""" - return (item for item in self.walk(*args, **kwargs) if item.is_dir()) + return (item for item in self.walk(match, errors) if item.is_dir()) - def walkfiles(self, *args, **kwargs): + def walkfiles(self, match: _Match = None, errors: str = 'strict') -> Iterator[Self]: """Iterator over files, recursively.""" - return (item for item in self.walk(*args, **kwargs) if item.is_file()) + return (item for item in self.walk(match, errors) if item.is_file()) - def fnmatch(self, pattern, normcase=None): + def fnmatch( + self, pattern: str, normcase: Callable[[str], str] | None = None + ) -> bool: """Return ``True`` if `self.name` matches the given `pattern`. `pattern` - A filename pattern with wildcards, @@ -608,7 +627,7 @@ def fnmatch(self, pattern, normcase=None): pattern = normcase(pattern) return fnmatch.fnmatchcase(name, pattern) - def glob(self, pattern): + def glob(self, pattern: str) -> list[Self]: """Return a list of Path objects that match the pattern. `pattern` - a path relative to this directory, with wildcards. @@ -625,7 +644,7 @@ def glob(self, pattern): cls = self._next_class return [cls(s) for s in glob.glob(self / pattern)] - def iglob(self, pattern): + def iglob(self, pattern: str) -> Iterator[Self]: """Return an iterator of Path objects that match the pattern. `pattern` - a path relative to this directory, with wildcards. @@ -655,7 +674,7 @@ def open(self, *args, **kwargs): """ return open(self, *args, **kwargs) - def bytes(self): + def bytes(self) -> builtins.bytes: """Open this file, read all bytes, return them as a string.""" with self.open('rb') as f: return f.read() @@ -716,7 +735,7 @@ def chunks(self, size, *args, **kwargs): with self.open(*args, **kwargs) as f: yield from iter(lambda: f.read(size) or None, None) - def write_bytes(self, bytes, append=False): + def write_bytes(self, bytes: builtins.bytes, append: bool = False) -> None: """Open this file and write the given bytes to it. Default behavior is to overwrite any existing file. @@ -725,7 +744,7 @@ def write_bytes(self, bytes, append=False): with self.open('ab' if append else 'wb') as f: f.write(bytes) - def read_text(self, encoding=None, errors=None): + def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: r"""Open this file, read it in, return the content as a string. Optional parameters are passed to :meth:`open`. @@ -735,7 +754,7 @@ def read_text(self, encoding=None, errors=None): with self.open(encoding=encoding, errors=errors) as f: return f.read() - def read_bytes(self): + def read_bytes(self) -> builtins.bytes: r"""Return the contents of this file as bytes.""" with self.open(mode='rb') as f: return f.read() @@ -804,7 +823,12 @@ def write_text( bytes = text.encode(encoding or sys.getdefaultencoding(), errors) self.write_bytes(bytes, append=append) - def lines(self, encoding=None, errors=None, retain=True): + def lines( + self, + encoding: str | None = None, + errors: str | None = None, + retain: bool = True, + ) -> list[str]: r"""Open this file, read all lines, return them in a list. Optional arguments: @@ -824,11 +848,11 @@ def lines(self, encoding=None, errors=None, retain=True): def write_lines( self, - lines, - encoding=None, - errors='strict', + lines: list[str], + encoding: str | None = None, + errors: str = 'strict', *, - append=False, + append: bool = False, ): r"""Write the given lines of text to this file. @@ -852,10 +876,10 @@ def write_lines( f.writelines(self._replace_linesep(lines)) @staticmethod - def _replace_linesep(lines): + def _replace_linesep(lines: Iterable[str]) -> Iterator[str]: return (line + os.linesep for line in _strip_newlines(lines)) - def read_md5(self): + def read_md5(self) -> builtins.bytes: """Calculate the md5 hash for this file. This reads through the entire file. @@ -864,7 +888,7 @@ def read_md5(self): """ return self.read_hash('md5') - def _hash(self, hash_name): + def _hash(self, hash_name: str) -> hashlib._Hash: """Returns a hash object for the file at the current path. `hash_name` should be a hash algo name (such as ``'md5'`` @@ -875,7 +899,7 @@ def _hash(self, hash_name): m.update(chunk) return m - def read_hash(self, hash_name): + def read_hash(self, hash_name) -> builtins.bytes: """Calculate given hash for this file. List of supported hashes can be obtained from :mod:`hashlib` package. @@ -885,7 +909,7 @@ def read_hash(self, hash_name): """ return self._hash(hash_name).digest() - def read_hexhash(self, hash_name): + def read_hexhash(self, hash_name) -> str: """Calculate given hash for this file, returning hexdigest. List of supported hashes can be obtained from :mod:`hashlib` package. @@ -900,7 +924,7 @@ def read_hexhash(self, hash_name): # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get # bound. Playing it safe and wrapping them all in method calls. - def isabs(self): + def isabs(self) -> bool: """ >>> Path('.').isabs() False @@ -909,23 +933,23 @@ def isabs(self): """ return self.module.isabs(self) - def exists(self): + def exists(self) -> bool: """.. seealso:: :func:`os.path.exists`""" return self.module.exists(self) - def is_dir(self): + def is_dir(self) -> bool: """.. seealso:: :func:`os.path.isdir`""" return self.module.isdir(self) - def is_file(self): + def is_file(self) -> bool: """.. seealso:: :func:`os.path.isfile`""" return self.module.isfile(self) - def islink(self): + def islink(self) -> bool: """.. seealso:: :func:`os.path.islink`""" return self.module.islink(self) - def ismount(self): + def ismount(self) -> bool: """ >>> Path('.').ismount() False @@ -934,15 +958,15 @@ def ismount(self): """ return self.module.ismount(self) - def samefile(self, other): + def samefile(self, other: str) -> bool: """.. seealso:: :func:`os.path.samefile`""" return self.module.samefile(self, other) - def getatime(self): + def getatime(self) -> float: """.. seealso:: :attr:`atime`, :func:`os.path.getatime`""" return self.module.getatime(self) - def set_atime(self, value): + def set_atime(self, value: Number | datetime.datetime): mtime_ns = self.stat().st_atime_ns self.utime(ns=(_make_timestamp_ns(value), mtime_ns)) @@ -968,11 +992,11 @@ def set_atime(self, value): """, ) - def getmtime(self): + def getmtime(self) -> float: """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`""" return self.module.getmtime(self) - def set_mtime(self, value): + def set_mtime(self, value: Number | datetime.datetime) -> None: atime_ns = self.stat().st_atime_ns self.utime(ns=(atime_ns, _make_timestamp_ns(value))) @@ -995,7 +1019,7 @@ def set_mtime(self, value): """, ) - def getctime(self): + def getctime(self) -> float: """.. seealso:: :attr:`ctime`, :func:`os.path.getctime`""" return self.module.getctime(self) @@ -1009,7 +1033,7 @@ def getctime(self): """, ) - def getsize(self): + def getsize(self) -> int: """.. seealso:: :attr:`size`, :func:`os.path.getsize`""" return self.module.getsize(self) @@ -1038,7 +1062,14 @@ def permissions(self) -> masks.Permissions: """ return masks.Permissions(self.stat().st_mode) - def access(self, *args, **kwargs): + def access( + self, + mode: int, + *, + dir_fd: int | None = None, + effective_ids: bool = False, + follow_symlinks: bool = True, + ) -> bool: """ Return does the real user have access to this path. @@ -1047,9 +1078,15 @@ def access(self, *args, **kwargs): .. seealso:: :func:`os.access` """ - return os.access(self, *args, **kwargs) + return os.access( + self, + mode, + dir_fd=dir_fd, + effective_ids=effective_ids, + follow_symlinks=follow_symlinks, + ) - def stat(self, *, follow_symlinks=True): + def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: """ Perform a ``stat()`` system call on this path. @@ -1060,7 +1097,7 @@ def stat(self, *, follow_symlinks=True): """ return os.stat(self, follow_symlinks=follow_symlinks) - def lstat(self): + def lstat(self) -> os.stat_result: """ Like :meth:`stat`, but do not follow symbolic links. @@ -1071,7 +1108,7 @@ def lstat(self): """ return os.lstat(self) - def __get_owner_windows(self): # pragma: nocover + def __get_owner_windows(self) -> str: # pragma: nocover r""" Return the name of the owner of this file or directory. Follow symbolic links. @@ -1087,7 +1124,7 @@ def __get_owner_windows(self): # pragma: nocover account, domain, typecode = win32security.LookupAccountSid(None, sid) return domain + '\\' + account - def __get_owner_unix(self): # pragma: nocover + def __get_owner_unix(self) -> str: # pragma: nocover """ Return the name of the owner of this file or directory. Follow symbolic links. @@ -1097,7 +1134,7 @@ def __get_owner_unix(self): # pragma: nocover st = self.stat() return pwd.getpwuid(st.st_uid).pw_name - def __get_owner_not_implemented(self): # pragma: nocover + def __get_owner_not_implemented(self) -> Never: # pragma: nocover raise NotImplementedError("Ownership not available on this platform.") get_owner = ( @@ -1119,7 +1156,7 @@ def __get_owner_not_implemented(self): # pragma: nocover if 'grp' in globals(): # pragma: no cover - def group(self, *, follow_symlinks=True): + def group(self, *, follow_symlinks: bool = True) -> str: """ Return the group name of the file gid. """ @@ -1128,7 +1165,7 @@ def group(self, *, follow_symlinks=True): if hasattr(os, 'statvfs'): - def statvfs(self): + def statvfs(self) -> os.statvfs_result: """Perform a ``statvfs()`` system call on this path. .. seealso:: :func:`os.statvfs` @@ -1137,22 +1174,29 @@ def statvfs(self): if hasattr(os, 'pathconf'): - def pathconf(self, name): + def pathconf(self, name: str | int) -> int: """.. seealso:: :func:`os.pathconf`""" return os.pathconf(self, name) # # --- Modifying operations on files and directories - def utime(self, *args, **kwargs): + def utime( + self, + times: tuple[int, int] | tuple[float, float] | None = None, + *, + ns: tuple[int, int] = ..., + dir_fd: int | None = None, + follow_symlinks: bool = True, + ) -> Self: """Set the access and modified times of this file. .. seealso:: :func:`os.utime` """ - os.utime(self, *args, **kwargs) + os.utime(self, times, ns=ns, dir_fd=dir_fd, follow_symlinks=follow_symlinks) return self - def chmod(self, mode): + def chmod(self, mode: str | int) -> Self: """ Set the mode. May be the new mode (os.chmod behavior) or a `symbolic mode `_. @@ -1173,7 +1217,7 @@ def chmod(self, mode): if hasattr(os, 'chown'): - def chown(self, uid=-1, gid=-1): + def chown(self, uid: str | int = -1, gid: str | int = -1) -> Self: """ Change the owner and group by names or numbers. @@ -1189,17 +1233,17 @@ def resolve_gid(gid): os.chown(self, resolve_uid(uid), resolve_gid(gid)) return self - def rename(self, new): + def rename(self, new: str) -> Self: """.. seealso:: :func:`os.rename`""" os.rename(self, new) return self._next_class(new) - def renames(self, new): + def renames(self, new: str) -> Self: """.. seealso:: :func:`os.renames`""" os.renames(self, new) return self._next_class(new) - def replace(self, target_or_old: Path | str, *args) -> Path: + def replace(self, target_or_old: str, *args) -> Self: """ Replace a path or substitute substrings. @@ -1236,36 +1280,36 @@ def replace(self, target_or_old: Path | str, *args) -> Path: # # --- Create/delete operations on directories - def mkdir(self, mode=0o777): + def mkdir(self, mode: int = 0o777) -> Self: """.. seealso:: :func:`os.mkdir`""" os.mkdir(self, mode) return self - def mkdir_p(self, mode=0o777): + def mkdir_p(self, mode: int = 0o777) -> Self: """Like :meth:`mkdir`, but does not raise an exception if the directory already exists.""" with contextlib.suppress(FileExistsError): self.mkdir(mode) return self - def makedirs(self, mode=0o777): + def makedirs(self, mode: int = 0o777) -> Self: """.. seealso:: :func:`os.makedirs`""" os.makedirs(self, mode) return self - def makedirs_p(self, mode=0o777): + def makedirs_p(self, mode: int = 0o777) -> Self: """Like :meth:`makedirs`, but does not raise an exception if the directory already exists.""" with contextlib.suppress(FileExistsError): self.makedirs(mode) return self - def rmdir(self): + def rmdir(self) -> Self: """.. seealso:: :func:`os.rmdir`""" os.rmdir(self) return self - def rmdir_p(self): + def rmdir_p(self) -> Self: """Like :meth:`rmdir`, but does not raise an exception if the directory is not empty or does not exist.""" suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty @@ -1274,12 +1318,12 @@ def rmdir_p(self): self.rmdir() return self - def removedirs(self): + def removedirs(self) -> Self: """.. seealso:: :func:`os.removedirs`""" os.removedirs(self) return self - def removedirs_p(self): + def removedirs_p(self) -> Self: """Like :meth:`removedirs`, but does not raise an exception if the directory is not empty or does not exist.""" with contextlib.suppress(FileExistsError, DirectoryNotEmpty): @@ -1289,7 +1333,7 @@ def removedirs_p(self): # --- Modifying operations on files - def touch(self): + def touch(self) -> Self: """Set the access/modified times of this file to the current time. Create the file if it does not exist. """ @@ -1297,12 +1341,12 @@ def touch(self): os.utime(self, None) return self - def remove(self): + def remove(self) -> Self: """.. seealso:: :func:`os.remove`""" os.remove(self) return self - def remove_p(self): + def remove_p(self) -> Self: """Like :meth:`remove`, but does not raise an exception if the file does not exist.""" with contextlib.suppress(FileNotFoundError): @@ -1322,7 +1366,7 @@ def hardlink_to(self, target: str) -> None: """ os.link(target, self) - def link(self, newpath): + def link(self, newpath: str) -> Self: """Create a hard link at `newpath`, pointing to this file. .. seealso:: :func:`os.link` @@ -1338,7 +1382,7 @@ def symlink_to(self, target: str, target_is_directory: bool = False) -> None: """ os.symlink(target, self, target_is_directory) - def symlink(self, newlink=None): + def symlink(self, newlink: str | None = None) -> Self: """Create a symbolic link at `newlink`, pointing here. If newlink is not supplied, the symbolic link will assume @@ -1351,7 +1395,7 @@ def symlink(self, newlink=None): os.symlink(self, newlink) return self._next_class(newlink) - def readlink(self): + def readlink(self) -> Self: """Return the path to which this symbolic link points. The result may be an absolute or a relative path. @@ -1360,7 +1404,7 @@ def readlink(self): """ return self._next_class(removeprefix(os.readlink(self), '\\\\?\\')) - def readlinkabs(self): + def readlinkabs(self) -> Self: """Return the path to which this symbolic link points. The result is always an absolute path. @@ -1384,14 +1428,14 @@ def readlinkabs(self): move = shutil.move rmtree = shutil.rmtree - def rmtree_p(self): + def rmtree_p(self) -> Self: """Like :meth:`rmtree`, but does not raise an exception if the directory does not exist.""" with contextlib.suppress(FileNotFoundError): self.rmtree() return self - def chdir(self): + def chdir(self) -> None: """.. seealso:: :func:`os.chdir`""" os.chdir(self) @@ -1399,11 +1443,12 @@ def chdir(self): def merge_tree( self, - dst, - symlinks=False, + dst: str, + symlinks: bool = False, *, - copy_function=shutil.copy2, - ignore=lambda dir, contents: [], + copy_function: Callable[[str, str], None] = shutil.copy2, + ignore: Callable[[Any, list[str]], list[str] | set[str]] = lambda dir, + contents: [], ): """ Copy entire contents of self to dst, overwriting existing @@ -1449,15 +1494,15 @@ def ignored(item): if hasattr(os, 'chroot'): - def chroot(self): # pragma: nocover + def chroot(self) -> None: # pragma: nocover """.. seealso:: :func:`os.chroot`""" os.chroot(self) if hasattr(os, 'startfile'): - def startfile(self, *args, **kwargs): # pragma: nocover + def startfile(self, operation: str | None = None) -> Self: # pragma: nocover """.. seealso:: :func:`os.startfile`""" - os.startfile(self, *args, **kwargs) + os.startfile(self, operation=operation) return self # in-place re-writing, courtesy of Martijn Pieters @@ -1465,13 +1510,13 @@ def startfile(self, *args, **kwargs): # pragma: nocover @contextlib.contextmanager def in_place( self, - mode='r', - buffering=-1, - encoding=None, - errors=None, - newline=None, - backup_extension=None, - ): + mode: str = 'r', + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + backup_extension: str | None = None, + ) -> Iterator[tuple[IO[Any], IO[Any]]]: """ A context in which a file may be re-written in-place with new content. @@ -1546,7 +1591,7 @@ def in_place( @classes.ClassProperty @classmethod - def special(cls): + def special(cls) -> Callable[[str | None], SpecialResolver]: """ Return a SpecialResolver object suitable referencing a suitable directory for the relevant platform for the given @@ -1573,7 +1618,7 @@ def special(cls): class DirectoryNotEmpty(OSError): @staticmethod @contextlib.contextmanager - def translate(): + def translate() -> Iterator[None]: try: yield except OSError as exc: @@ -1582,7 +1627,7 @@ def translate(): raise -def only_newer(copy_func): +def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: """ Wrap a copy function (like shutil.copy2) to return the dst if it's newer than the source. @@ -1607,7 +1652,7 @@ class ExtantPath(Path): OSError: does-not-exist does not exist. """ - def _validate(self): + def _validate(self) -> None: if not self.exists(): raise OSError(f"{self} does not exist.") @@ -1622,31 +1667,43 @@ class ExtantFile(Path): FileNotFoundError: does-not-exist does not exist as a file. """ - def _validate(self): + def _validate(self) -> None: if not self.is_file(): raise FileNotFoundError(f"{self} does not exist as a file.") class SpecialResolver: class ResolverScope: - def __init__(self, paths, scope): + def __init__(self, paths: SpecialResolver, scope: str) -> None: self.paths = paths self.scope = scope - def __getattr__(self, class_): + def __getattr__(self, class_: str) -> _MultiPathType: return self.paths.get_dir(self.scope, class_) - def __init__(self, path_class, *args, **kwargs): + def __init__( + self, + path_class: type, + appname: str | None = None, + appauthor: str | None = None, + version: str | None = None, + roaming: bool = False, + multipath: bool = False, + ): appdirs = importlib.import_module('appdirs') - - vars(self).update( - path_class=path_class, wrapper=appdirs.AppDirs(*args, **kwargs) + self.path_class = path_class + self.wrapper = appdirs.AppDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + multipath=multipath, ) - def __getattr__(self, scope): + def __getattr__(self, scope: str) -> ResolverScope: return self.ResolverScope(self, scope) - def get_dir(self, scope, class_): + def get_dir(self, scope: str, class_: str) -> _MultiPathType: """ Return the callable function from appdirs, but with the result wrapped in self.path_class @@ -1663,28 +1720,32 @@ class Multi: """ @classmethod - def for_class(cls, path_cls): + def for_class(cls, path_cls: type) -> type[_MultiPathType]: name = 'Multi' + path_cls.__name__ return type(name, (cls, path_cls), {}) @classmethod - def detect(cls, input): + def detect(cls, input: str) -> _MultiPathType: if os.pathsep not in input: cls = cls._next_class return cls(input) - def __iter__(self): + def __iter__(self) -> Iterator[Path]: return iter(map(self._next_class, self.split(os.pathsep))) @classes.ClassProperty @classmethod - def _next_class(cls): + def _next_class(cls) -> type[Path]: """ Multi-subclasses should use the parent class """ return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi)) +class _MultiPathType(Multi, Path): + pass + + class TempDir(Path): """ A temporary directory via :func:`tempfile.mkdtemp`, and @@ -1707,39 +1768,44 @@ class TempDir(Path): @classes.ClassProperty @classmethod - def _next_class(cls): + def _next_class(cls) -> type[Path]: return Path - def __new__(cls, *args, **kwargs): + def __new__(cls, *args, **kwargs) -> Self: dirname = tempfile.mkdtemp(*args, **kwargs) return super().__new__(cls, dirname) - def __init__(self, *args, **kwargs): + def __init__(self) -> None: pass - def __enter__(self): + def __enter__(self) -> Self: # TempDir should return a Path version of itself and not itself # so that a second context manager does not create a second # temporary directory, but rather changes CWD to the location # of the temporary directory. return self._next_class(self) - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: self.rmtree() class Handlers: - def strict(msg): + def strict(msg: str) -> Never: raise - def warn(msg): + def warn(msg: str) -> None: warnings.warn(msg, TreeWalkWarning, stacklevel=2) - def ignore(msg): + def ignore(msg: str) -> None: pass @classmethod - def _resolve(cls, param): + def _resolve(cls, param: str | Callable[[str], None]) -> Callable[[str], None]: if not callable(param) and param not in vars(Handlers): raise ValueError("invalid errors parameter") return vars(cls).get(param, param) diff --git a/path/__init__.pyi b/path/__init__.pyi deleted file mode 100644 index d88187cc..00000000 --- a/path/__init__.pyi +++ /dev/null @@ -1,330 +0,0 @@ -from __future__ import annotations - -import builtins -import contextlib -import os -import sys -from types import ModuleType, TracebackType -from typing import ( - IO, - Any, - AnyStr, - Callable, - Generator, - Iterable, - Iterator, -) - -from typing_extensions import Self - -from . import classes - -# Type for the match argument for several methods -_Match = str | Callable[[str], bool] | Callable[[Path], bool] | None - -class TreeWalkWarning(Warning): - pass - -class Traversal: - follow: Callable[[Path], bool] - - def __init__(self, follow: Callable[[Path], bool]): ... - def __call__( - self, - walker: Generator[Path, Callable[[], bool] | None, None], - ) -> Iterator[Path]: ... - -class Path(str): - module: Any - - def __init__(self, other: Any = ...) -> None: ... - @classmethod - def using_module(cls, module: ModuleType) -> type[Path]: ... - @classes.ClassProperty - @classmethod - def _next_class(cls) -> type[Self]: ... - def __repr__(self) -> str: ... - def __add__(self, more: str) -> Self: ... - def __radd__(self, other: str) -> Self: ... - def __div__(self, rel: str) -> Self: ... - def __truediv__(self, rel: str) -> Self: ... - def __rdiv__(self, rel: str) -> Self: ... - def __rtruediv__(self, rel: str) -> Self: ... - def __enter__(self) -> Self: ... - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: ... - def absolute(self) -> Self: ... - def normcase(self) -> Self: ... - def normpath(self) -> Self: ... - def realpath(self) -> Self: ... - def expanduser(self) -> Self: ... - def expandvars(self) -> Self: ... - def dirname(self) -> Self: ... - def basename(self) -> Self: ... - def expand(self) -> Self: ... - @property - def stem(self) -> str: ... - def with_stem(self, stem: str) -> Self: ... - @property - def suffix(self) -> Self: ... - def with_suffix(self, suffix: str) -> Self: ... - @property - def drive(self) -> Self: ... - @property - def parent(self) -> Self: ... - @property - def name(self) -> Self: ... - def with_name(self, name: str) -> Self: ... - def splitpath(self) -> tuple[Self, str]: ... - def splitdrive(self) -> tuple[Self, Self]: ... - def splitext(self) -> tuple[Self, str]: ... - def stripext(self) -> Self: ... - @classes.multimethod - def joinpath(cls, first: str, *others: str) -> Self: ... - def splitall(self) -> list[Self | str]: ... - def parts(self) -> tuple[Self | str, ...]: ... - def _parts(self) -> Iterator[Self | str]: ... - def _parts_iter(self) -> Iterator[Self | str]: ... - def relpath(self, start: str = ...) -> Self: ... - def relpathto(self, dest: str) -> Self: ... - - # --- Listing, searching, walking, and matching - def iterdir(self, match: _Match = ...) -> Iterator[Self]: ... - def dirs(self, match: _Match = ...) -> list[Self]: ... - def files(self, match: _Match = ...) -> list[Self]: ... - def walk( - self, - match: _Match = ..., - errors: str = ..., - ) -> Generator[Self, Callable[[], bool] | None, None]: ... - def walkdirs( - self, - match: _Match = ..., - errors: str = ..., - ) -> Iterator[Self]: ... - def walkfiles( - self, - match: _Match = ..., - errors: str = ..., - ) -> Iterator[Self]: ... - def fnmatch( - self, - pattern: Path | str, - normcase: Callable[[str], str] | None = ..., - ) -> bool: ... - def glob(self, pattern: str) -> list[Self]: ... - def iglob(self, pattern: str) -> Iterator[Self]: ... - def bytes(self) -> builtins.bytes: ... - def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... - def read_text( - self, encoding: str | None = ..., errors: str | None = ... - ) -> str: ... - def read_bytes(self) -> builtins.bytes: ... - def lines( - self, - encoding: str | None = ..., - errors: str | None = ..., - retain: bool = ..., - ) -> list[str]: ... - def write_lines( - self, - lines: list[str], - encoding: str | None = ..., - errors: str = ..., - *, - append: bool = ..., - ) -> None: ... - def read_md5(self) -> builtins.bytes: ... - def read_hash(self, hash_name: str) -> builtins.bytes: ... - def read_hexhash(self, hash_name: str) -> str: ... - def isabs(self) -> bool: ... - def exists(self) -> bool: ... - def is_dir(self) -> bool: ... - def is_file(self) -> bool: ... - def islink(self) -> bool: ... - def ismount(self) -> bool: ... - def samefile(self, other: str) -> bool: ... - def getatime(self) -> float: ... - @property - def atime(self) -> float: ... - def getmtime(self) -> float: ... - @property - def mtime(self) -> float: ... - def getctime(self) -> float: ... - @property - def ctime(self) -> float: ... - def getsize(self) -> int: ... - @property - def size(self) -> int: ... - def access( - self, - mode: int, - *, - dir_fd: int | None = ..., - effective_ids: bool = ..., - follow_symlinks: bool = ..., - ) -> bool: ... - def stat(self) -> os.stat_result: ... - def lstat(self) -> os.stat_result: ... - def get_owner(self) -> str: ... - @property - def owner(self) -> str: ... - - if sys.platform != 'win32': - def statvfs(self) -> os.statvfs_result: ... - def pathconf(self, name: str | int) -> int: ... - - def utime( - self, - times: tuple[int, int] | tuple[float, float] | None = ..., - *, - ns: tuple[int, int] = ..., - dir_fd: int | None = ..., - follow_symlinks: bool = ..., - ) -> Path: ... - def chmod(self, mode: str | int) -> Self: ... - - if sys.platform != 'win32': - def chown(self, uid: int | str = ..., gid: int | str = ...) -> Self: ... - - def rename(self, new: str) -> Self: ... - def renames(self, new: str) -> Self: ... - def mkdir(self, mode: int = ...) -> Self: ... - def mkdir_p(self, mode: int = ...) -> Self: ... - def makedirs(self, mode: int = ...) -> Self: ... - def makedirs_p(self, mode: int = ...) -> Self: ... - def rmdir(self) -> Self: ... - def rmdir_p(self) -> Self: ... - def removedirs(self) -> Self: ... - def removedirs_p(self) -> Self: ... - def touch(self) -> Self: ... - def remove(self) -> Self: ... - def remove_p(self) -> Self: ... - def unlink(self) -> Self: ... - def unlink_p(self) -> Self: ... - def link(self, newpath: str) -> Self: ... - def symlink(self, newlink: str | None = ...) -> Self: ... - def readlink(self) -> Self: ... - def readlinkabs(self) -> Self: ... - def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... - def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copy(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... - def copy2(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... - def copytree( - self, - dst: str, - symlinks: bool = ..., - ignore: Callable[[str, list[str]], Iterable[str]] | None = ..., - copy_function: Callable[[str, str], None] = ..., - ignore_dangling_symlinks: bool = ..., - dirs_exist_ok: bool = ..., - ) -> Any: ... - def move( - self, dst: str, copy_function: Callable[[str, str], None] = ... - ) -> Any: ... - def rmtree( - self, - ignore_errors: bool = ..., - onerror: Callable[[Any, Any, Any], Any] | None = ..., - ) -> None: ... - def rmtree_p(self) -> Self: ... - def chdir(self) -> None: ... - def cd(self) -> None: ... - def merge_tree( - self, - dst: str, - symlinks: bool = ..., - *, - copy_function: Callable[[str, str], None] = ..., - ignore: Callable[[Any, list[str]], list[str] | set[str]] = ..., - ) -> None: ... - - if sys.platform != 'win32': - def chroot(self) -> None: ... - - if sys.platform == 'win32': - def startfile(self, operation: str | None = ...) -> Self: ... - - @contextlib.contextmanager - def in_place( - self, - mode: str = ..., - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - backup_extension: str | None = ..., - ) -> Iterator[tuple[IO[Any], IO[Any]]]: ... - @classes.ClassProperty - @classmethod - def special(cls) -> Callable[[str | None], SpecialResolver]: ... - -class DirectoryNotEmpty(OSError): - @staticmethod - def translate() -> Iterator[None]: ... - -def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: ... - -class ExtantPath(Path): - def _validate(self) -> None: ... - -class ExtantFile(Path): - def _validate(self) -> None: ... - -class SpecialResolver: - class ResolverScope: - def __init__(self, paths: SpecialResolver, scope: str) -> None: ... - def __getattr__(self, class_: str) -> MultiPathType: ... - - def __init__( - self, - path_class: type, - appname: str | None = ..., - appauthor: str | None = ..., - version: str | None = ..., - roaming: bool = ..., - multipath: bool = ..., - ): ... - def __getattr__(self, scope: str) -> ResolverScope: ... - def get_dir(self, scope: str, class_: str) -> MultiPathType: ... - -class Multi: - @classmethod - def for_class(cls, path_cls: type) -> type[MultiPathType]: ... - @classmethod - def detect(cls, input: str) -> MultiPathType: ... - def __iter__(self) -> Iterator[Path]: ... - @classes.ClassProperty - @classmethod - def _next_class(cls) -> type[Path]: ... - -class MultiPathType(Multi, Path): - pass - -class TempDir(Path): - @classes.ClassProperty - @classmethod - def _next_class(cls) -> type[Path]: ... - def __new__( - cls, - suffix: AnyStr | None = ..., - prefix: AnyStr | None = ..., - dir: AnyStr | os.PathLike[AnyStr] | None = ..., - ) -> Self: ... - def __init__(self) -> None: ... - def __enter__(self) -> Self: ... - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: ... - -class Handlers: - @classmethod - def _resolve(cls, param: str | Callable[[str], None]) -> Callable[[str], None]: ... From ee57d431ce1491d4a557c7e55f07bfc7e84f399d Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 20:47:26 -0800 Subject: [PATCH 777/835] Correct annotation for Path.utime The ns keyword argument can be either present or not, but it has no default. To specify this requires overloads and **kwargs. --- path/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index de7c2aa3..d5793e9f 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1181,19 +1181,30 @@ def pathconf(self, name: str | int) -> int: # # --- Modifying operations on files and directories + @overload def utime( self, times: tuple[int, int] | tuple[float, float] | None = None, *, - ns: tuple[int, int] = ..., dir_fd: int | None = None, follow_symlinks: bool = True, - ) -> Self: + ) -> Self: ... + @overload + def utime( + self, + times: tuple[int, int] | tuple[float, float] | None = None, + *, + ns: tuple[int, int], + dir_fd: int | None = None, + follow_symlinks: bool = True, + ) -> Self: ... + + def utime(self, *args, **kwargs) -> Self: """Set the access and modified times of this file. .. seealso:: :func:`os.utime` """ - os.utime(self, times, ns=ns, dir_fd=dir_fd, follow_symlinks=follow_symlinks) + os.utime(self, *args, **kwargs) return self def chmod(self, mode: str | int) -> Self: From d373f87846f0391ce65d9cf69ba3bb9331a11aaa Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 20:56:35 -0800 Subject: [PATCH 778/835] Eliminate attribute error type hint for _validate The base Path does not have a _validate, but subclasses do. The base Path.__init__ calls _validate, but wraps it in an AttributeError suppression. mypy still throws an error despite. To solve, a do-nothing _validate function has been added to Path itself, and the AttributeError suppression has been removed. --- path/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index d5793e9f..dd78ea32 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -172,8 +172,10 @@ def __new__(cls, other='.'): def __init__(self, other: Any = '.') -> None: if other is None: raise TypeError("Invalid initial value for path: None") - with contextlib.suppress(AttributeError): - self._validate() + self._validate() + + def _validate(self) -> None: + pass @classmethod @functools.lru_cache From 0e61b7f3752c84d15081ce8922362de67cee73bb Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:03:00 -0800 Subject: [PATCH 779/835] Replace Number with float or int The Number ABC is more abstract than needed when defining allowed input types - float captures both float and int. Further, when returning explictly an int, the return type should be annotated as such instead of as Number. This also eliminates a mypy error about multiplying a Number with a float. --- path/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index dd78ea32..f499b60d 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -41,7 +41,6 @@ import sys import tempfile import warnings -from numbers import Number from types import ModuleType, TracebackType with contextlib.suppress(ImportError): @@ -89,8 +88,8 @@ _default_linesep = object() -def _make_timestamp_ns(value: Number | datetime.datetime) -> Number: - timestamp_s = value if isinstance(value, Number) else value.timestamp() +def _make_timestamp_ns(value: float | datetime.datetime) -> int: + timestamp_s = value if isinstance(value, (float, int)) else value.timestamp() return int(timestamp_s * 10**9) @@ -968,7 +967,7 @@ def getatime(self) -> float: """.. seealso:: :attr:`atime`, :func:`os.path.getatime`""" return self.module.getatime(self) - def set_atime(self, value: Number | datetime.datetime): + def set_atime(self, value: float | datetime.datetime): mtime_ns = self.stat().st_atime_ns self.utime(ns=(_make_timestamp_ns(value), mtime_ns)) @@ -998,7 +997,7 @@ def getmtime(self) -> float: """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`""" return self.module.getmtime(self) - def set_mtime(self, value: Number | datetime.datetime) -> None: + def set_mtime(self, value: float | datetime.datetime) -> None: atime_ns = self.stat().st_atime_ns self.utime(ns=(atime_ns, _make_timestamp_ns(value))) From ee0084dc7ee7d09140ac0f52c7042e6982805c59 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:06:51 -0800 Subject: [PATCH 780/835] Fix error WRT to returned tuple elements --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index f499b60d..537e1798 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -452,7 +452,7 @@ def splitall(self) -> list[Self | str]: """ return list(self._parts()) - def parts(self) -> tuple[Self | str]: + def parts(self) -> tuple[Self | str, ...]: """ >>> Path('/foo/bar/baz').parts() (Path('/'), 'foo', 'bar', 'baz') From 243f2806db8e8ab390d2cc9ba20952d91ac60f92 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:11:21 -0800 Subject: [PATCH 781/835] Cannot reassign different types to the same name mypy has a hard time if the same name is used to hold more than one type, so where this error was occuring a new name is used for the second type. --- path/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 537e1798..bab12070 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -487,15 +487,15 @@ def relpathto(self, dest: str) -> Self: ``dest.absolute()``. """ origin = self.absolute() - dest = self._next_class(dest).absolute() + dest_path = self._next_class(dest).absolute() orig_list = origin.normcase().splitall() # Don't normcase dest! We want to preserve the case. - dest_list = dest.splitall() + dest_list = dest_path.splitall() if orig_list[0] != self.module.normcase(dest_list[0]): # Can't get here from there. - return dest + return dest_path # Find the location where the two paths start to differ. i = 0 @@ -1475,8 +1475,8 @@ def merge_tree( src.merge_tree(dst, copy_function=only_newer(shutil.copy2)) """ - dst = self._next_class(dst) - dst.makedirs_p() + dst_path = self._next_class(dst) + dst_path.makedirs_p() sources = list(self.iterdir()) _ignored = ignore(self, [item.name for item in sources]) @@ -1485,7 +1485,7 @@ def ignored(item): return item.name in _ignored for source in itertools.filterfalse(ignored, sources): - dest = dst / source.name + dest = dst_path / source.name if symlinks and source.islink(): target = source.readlink() target.symlink(dest) @@ -1499,7 +1499,7 @@ def ignored(item): else: copy_function(source, dest) - self.copystat(dst) + self.copystat(dst_path) # # --- Special stuff from os From da5093ec6891bfb1ac40db9876a07bbeb5fde6c8 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:24:46 -0800 Subject: [PATCH 782/835] Correct annotations around Handlers The following changes had to be made: - In Path.walk, Handlers._resolve transformed "errors" from a string into a function, which mypy does not like. To resolve, it now assignes "error_fn". - In the recursive Path.walk call, error_fn is not of type str for the errors argument, which would be a typing violation - this has been explictly ignored so as to not make the function option public. - mypy likes to use isinstance to differentiate types, so the logic for Handers._resolve was made more mypy-friendly so that it properly understood the types it was dealing with. - To avoid expected argument errors, the "public" Handlers functions have been made into staticmethods. --- path/__init__.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index bab12070..53315c9c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -576,13 +576,13 @@ def walk( `errors` may also be an arbitrary callable taking a msg parameter. """ - errors = Handlers._resolve(errors) + error_fn = Handlers._resolve(errors) match = matchers.load(match) try: childList = self.iterdir() except Exception as exc: - errors(f"Unable to list directory '{self}': {exc}") + error_fn(f"Unable to list directory '{self}': {exc}") return for child in childList: @@ -593,11 +593,11 @@ def walk( try: do_traverse = traverse() except Exception as exc: - errors(f"Unable to access '{child}': {exc}") + error_fn(f"Unable to access '{child}': {exc}") continue if do_traverse: - yield from child.walk(errors=errors, match=match) + yield from child.walk(errors=error_fn, match=match) # type: ignore[arg-type] def walkdirs(self, match: _Match = None, errors: str = 'strict') -> Iterator[Self]: """Iterator over subdirs, recursively.""" @@ -1807,17 +1807,26 @@ def __exit__( class Handlers: + @staticmethod def strict(msg: str) -> Never: raise + @staticmethod def warn(msg: str) -> None: warnings.warn(msg, TreeWalkWarning, stacklevel=2) + @staticmethod def ignore(msg: str) -> None: pass @classmethod def _resolve(cls, param: str | Callable[[str], None]) -> Callable[[str], None]: - if not callable(param) and param not in vars(Handlers): - raise ValueError("invalid errors parameter") - return vars(cls).get(param, param) + msg = "invalid errors parameter" + if isinstance(param, str): + if param not in vars(cls): + raise ValueError(msg) + return {"strict": cls.strict, "warn": cls.warn, "ignore": cls.ignore}[param] + else: + if not callable(param): + raise ValueError(msg) + return param From e191025c8f1ed884ac7e31765878451e8d4b0216 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:37:19 -0800 Subject: [PATCH 783/835] Remove unneeded explict arguments for __exit__ --- path/__init__.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 53315c9c..87b89a03 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -41,7 +41,7 @@ import sys import tempfile import warnings -from types import ModuleType, TracebackType +from types import ModuleType with contextlib.suppress(ImportError): import win32security @@ -231,12 +231,7 @@ def __enter__(self) -> Self: os.chdir(self) return self - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: + def __exit__(self, *_) -> None: os.chdir(self._old_dir) @classmethod @@ -1797,12 +1792,7 @@ def __enter__(self) -> Self: # of the temporary directory. return self._next_class(self) - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: + def __exit__(self, *_) -> None: self.rmtree() From 318ef04b596dbd1fb79534c28300fd365e4a215f Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 21:46:20 -0800 Subject: [PATCH 784/835] Fully annotate open When calling e.g. "self.open()", mypy did not recognize that that the implict self argument was acting as the first argument to the builtin open function. Unfortunately, to make mypy fully understand the Path.open function a full list of the open overloads is required. --- path/__init__.py | 94 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 87b89a03..83cad9bb 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -41,6 +41,13 @@ import sys import tempfile import warnings +from io import ( + BufferedRandom, + BufferedReader, + BufferedWriter, + FileIO, + TextIOWrapper, +) from types import ModuleType with contextlib.suppress(ImportError): @@ -56,6 +63,7 @@ TYPE_CHECKING, IO, Any, + BinaryIO, Callable, Generator, Iterable, @@ -64,9 +72,12 @@ ) if TYPE_CHECKING: - from typing_extensions import Never, Self + from typing_extensions import Literal, Never, Self from _typeshed import ( OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, OpenTextMode, ) @@ -661,7 +672,83 @@ def iglob(self, pattern: str) -> Iterator[Self]: # # --- Reading or writing an entire file at once. - @functools.wraps(open, assigned=()) + @overload + def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> TextIOWrapper: ... + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> FileIO: ... + @overload + def open( + self, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedRandom: ... + @overload + def open( + self, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedWriter: ... + @overload + def open( + self, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedReader: ... + @overload + def open( + self, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BinaryIO: ... + @overload + def open( + self, + mode: str, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> IO[Any]: ... def open(self, *args, **kwargs): """Open this file and return a corresponding file object. @@ -687,7 +774,6 @@ def chunks( closefd: bool = ..., opener: Callable[[str, int], int] | None = ..., ) -> Iterator[str]: ... - @overload def chunks( self, @@ -700,7 +786,6 @@ def chunks( closefd: bool = ..., opener: Callable[[str, int], int] | None = ..., ) -> Iterator[builtins.bytes]: ... - @overload def chunks( self, @@ -713,7 +798,6 @@ def chunks( closefd: bool = ..., opener: Callable[[str, int], int] | None = ..., ) -> Iterator[str | builtins.bytes]: ... - def chunks(self, size, *args, **kwargs): """Returns a generator yielding chunks of the file, so it can be read piece by piece with a simple for loop. From 3c8382fbdece42999ab1028b6fcda604d86d047d Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 22:03:04 -0800 Subject: [PATCH 785/835] Correct call to contextlib.suppress A tuple of exceptions was passed to contextlib.suppress, but it actually needs to be individual arguments of exceptions - this has been corrected. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index 83cad9bb..266825e3 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1404,7 +1404,7 @@ def rmdir_p(self) -> Self: """Like :meth:`rmdir`, but does not raise an exception if the directory is not empty or does not exist.""" suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty - with contextlib.suppress(suppressed): + with contextlib.suppress(*suppressed): with DirectoryNotEmpty.translate(): self.rmdir() return self From 2a4adbf44e4141b8839ff7ef838fd23739b3fe21 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sun, 22 Dec 2024 22:36:17 -0800 Subject: [PATCH 786/835] Ignore type errors due to Multi mixin mypy has a hard knowing what is being returned with the mixin design pattern, so instead of attempting some sort of refactoring these errors are simply being ignored. --- path/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 266825e3..78251a5a 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1764,7 +1764,13 @@ def _validate(self) -> None: class SpecialResolver: + path_class: type + wrapper: ModuleType + class ResolverScope: + paths: SpecialResolver + scope: str + def __init__(self, paths: SpecialResolver, scope: str) -> None: self.paths = paths self.scope = scope @@ -1819,10 +1825,10 @@ def for_class(cls, path_cls: type) -> type[_MultiPathType]: def detect(cls, input: str) -> _MultiPathType: if os.pathsep not in input: cls = cls._next_class - return cls(input) + return cls(input) # type: ignore[return-value, call-arg] def __iter__(self) -> Iterator[Path]: - return iter(map(self._next_class, self.split(os.pathsep))) + return iter(map(self._next_class, self.split(os.pathsep))) # type: ignore[attr-defined] @classes.ClassProperty @classmethod @@ -1830,7 +1836,7 @@ def _next_class(cls) -> type[Path]: """ Multi-subclasses should use the parent class """ - return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi)) + return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi)) # type: ignore[return-value] class _MultiPathType(Multi, Path): From fc92f97b1e3aaf5fe3b6de2efc463ea7d667a76b Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 07:32:03 -0800 Subject: [PATCH 787/835] Use sys.platform to conditionally add methods mypy cannot use aribrary logic to conditionally run code during type checking - it mostly only uses isinstance, and checks against sys.version_info or sys.platform. The conditional inclusion of methods into Path have been changed from using hasattr or a globals check to sys.platform. Additionally, the conditional addition of shutil.move has been removed since shutil.move is always available. --- path/__init__.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 78251a5a..ebf27917 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1217,13 +1217,14 @@ def __get_owner_unix(self) -> str: # pragma: nocover def __get_owner_not_implemented(self) -> Never: # pragma: nocover raise NotImplementedError("Ownership not available on this platform.") - get_owner = ( - __get_owner_windows - if 'win32security' in globals() - else __get_owner_unix - if 'pwd' in globals() - else __get_owner_not_implemented - ) + if sys.platform != "win32": + get_owner = __get_owner_unix + else: + get_owner = ( + __get_owner_windows + if "win32security" in globals() + else __get_owner_not_implemented + ) owner = property( get_owner, @@ -1234,7 +1235,7 @@ def __get_owner_not_implemented(self) -> Never: # pragma: nocover .. seealso:: :meth:`get_owner`""", ) - if 'grp' in globals(): # pragma: no cover + if sys.platform != "win32": # pragma: no cover def group(self, *, follow_symlinks: bool = True) -> str: """ @@ -1243,8 +1244,6 @@ def group(self, *, follow_symlinks: bool = True) -> str: gid = self.stat(follow_symlinks=follow_symlinks).st_gid return grp.getgrgid(gid).gr_name - if hasattr(os, 'statvfs'): - def statvfs(self) -> os.statvfs_result: """Perform a ``statvfs()`` system call on this path. @@ -1252,8 +1251,6 @@ def statvfs(self) -> os.statvfs_result: """ return os.statvfs(self) - if hasattr(os, 'pathconf'): - def pathconf(self, name: str | int) -> int: """.. seealso:: :func:`os.pathconf`""" return os.pathconf(self, name) @@ -1306,7 +1303,7 @@ def chmod(self, mode: str | int) -> Self: os.chmod(self, mode) return self - if hasattr(os, 'chown'): + if sys.platform != "win32": def chown(self, uid: str | int = -1, gid: str | int = -1) -> Self: """ @@ -1515,8 +1512,7 @@ def readlinkabs(self) -> Self: copy = shutil.copy copy2 = shutil.copy2 copytree = shutil.copytree - if hasattr(shutil, 'move'): - move = shutil.move + move = shutil.move rmtree = shutil.rmtree def rmtree_p(self) -> Self: @@ -1583,13 +1579,13 @@ def ignored(item): # # --- Special stuff from os - if hasattr(os, 'chroot'): + if sys.platform != "win32": def chroot(self) -> None: # pragma: nocover """.. seealso:: :func:`os.chroot`""" os.chroot(self) - if hasattr(os, 'startfile'): + if sys.platform == "win32": def startfile(self, operation: str | None = None) -> Self: # pragma: nocover """.. seealso:: :func:`os.startfile`""" From ae3c7db073edcc76fafe547cfc22d512f490f763 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 08:31:53 -0800 Subject: [PATCH 788/835] Use property as a decorator to define properties When used in the functional form the typing system has a hard time determining the types of properties. Changing the definition to use the decorator form resolves this issue. --- path/__init__.py | 90 ++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index ebf27917..33895377 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -353,32 +353,28 @@ def drive(self) -> Self: drive, r = self.module.splitdrive(self) return self._next_class(drive) - parent = property( - dirname, - None, - None, - """ This path's parent directory, as a new Path object. + @property + def parent(self) -> Self: + """This path's parent directory, as a new Path object. For example, ``Path('/usr/local/lib/libpython.so').parent == Path('/usr/local/lib')`` .. seealso:: :meth:`dirname`, :func:`os.path.dirname` - """, - ) + """ + return self.dirname() - name = property( - basename, - None, - None, - """ The name of this file or directory without the full path. + @property + def name(self) -> Self: + """The name of this file or directory without the full path. For example, ``Path('/usr/local/lib/libpython.so').name == 'libpython.so'`` .. seealso:: :meth:`basename`, :func:`os.path.basename` - """, - ) + """ + return self.basename() def with_name(self, name: str) -> Self: """Return a new path with the name changed. @@ -1046,14 +1042,12 @@ def getatime(self) -> float: """.. seealso:: :attr:`atime`, :func:`os.path.getatime`""" return self.module.getatime(self) - def set_atime(self, value: float | datetime.datetime): + def set_atime(self, value: float | datetime.datetime) -> None: mtime_ns = self.stat().st_atime_ns self.utime(ns=(_make_timestamp_ns(value), mtime_ns)) - atime = property( - getatime, - set_atime, - None, + @property + def atime(self) -> float: """ Last access time of the file. @@ -1069,8 +1063,12 @@ def set_atime(self, value: float | datetime.datetime): 200336400.0 .. seealso:: :meth:`getatime`, :func:`os.path.getatime` - """, - ) + """ + return self.getatime() + + @atime.setter + def atime(self, value: float | datetime.datetime) -> None: + self.set_atime(value) def getmtime(self) -> float: """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`""" @@ -1080,10 +1078,8 @@ def set_mtime(self, value: float | datetime.datetime) -> None: atime_ns = self.stat().st_atime_ns self.utime(ns=(atime_ns, _make_timestamp_ns(value))) - mtime = property( - getmtime, - set_mtime, - None, + @property + def mtime(self) -> float: """ Last modified time of the file. @@ -1096,36 +1092,36 @@ def set_mtime(self, value: float | datetime.datetime) -> None: 200336400.0 .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime` - """, - ) + """ + return self.getmtime() + + @mtime.setter + def mtime(self, value: float | datetime.datetime) -> None: + self.set_mtime(value) def getctime(self) -> float: """.. seealso:: :attr:`ctime`, :func:`os.path.getctime`""" return self.module.getctime(self) - ctime = property( - getctime, - None, - None, - """ Creation time of the file. + @property + def ctime(self) -> float: + """Creation time of the file. .. seealso:: :meth:`getctime`, :func:`os.path.getctime` - """, - ) + """ + return self.getctime() def getsize(self) -> int: """.. seealso:: :attr:`size`, :func:`os.path.getsize`""" return self.module.getsize(self) - size = property( - getsize, - None, - None, - """ Size of the file, in bytes. + @property + def size(self) -> int: + """Size of the file, in bytes. .. seealso:: :meth:`getsize`, :func:`os.path.getsize` - """, - ) + """ + return self.getsize() @property def permissions(self) -> masks.Permissions: @@ -1226,14 +1222,12 @@ def __get_owner_not_implemented(self) -> Never: # pragma: nocover else __get_owner_not_implemented ) - owner = property( - get_owner, - None, - None, - """ Name of the owner of this file or directory. + @property + def owner(self) -> str: + """Name of the owner of this file or directory. - .. seealso:: :meth:`get_owner`""", - ) + .. seealso:: :meth:`get_owner`""" + return self.get_owner() if sys.platform != "win32": # pragma: no cover From cde1eaf191de7ac47cc3494558c708f6a1be00b3 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 12:54:20 -0800 Subject: [PATCH 789/835] Ensure shutil-derived functions are properly typed When assigning a shutil function directly into the Path namespace, the typing system seems to have trouble figuring out that the first argument is the implict self, and as a result the type checking fails for these functions. To solve, thin passthrough functions have been added so that the annotations are correct for the Path context, but the docstrings have been copied from shutil as-is. --- path/__init__.py | 142 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 127 insertions(+), 15 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 33895377..f98b8fcf 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -79,16 +79,20 @@ OpenBinaryModeUpdating, OpenBinaryModeWriting, OpenTextMode, + ExcInfo, ) + _Match = str | Callable[[str], bool] | None + _CopyFn = Callable[[str, str], object] + _OnErrorCallback = Callable[[Callable[..., Any], str, ExcInfo], object] + _OnExcCallback = Callable[[Callable[..., Any], str, BaseException], object] + + from . import classes, masks, matchers from .compat.py38 import removeprefix, removesuffix __all__ = ['Path', 'TempDir'] -# Type for the match argument for several methods -_Match = str | Callable[[str], bool] | None - LINESEPS = ['\r\n', '\r', '\n'] U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] B_NEWLINE = re.compile('|'.join(LINESEPS).encode()) @@ -1496,18 +1500,126 @@ def readlinkabs(self) -> Self: p = self.readlink() return p if p.isabs() else (self.parent / p).absolute() - # High-level functions from shutil - # These functions will be bound to the instance such that - # Path(name).copy(target) will invoke shutil.copy(name, target) + # High-level functions from shutil. + + def copyfile(self, dst: str, *, follow_symlinks: bool = True) -> Self: + return self._next_class( + shutil.copyfile(self, dst, follow_symlinks=follow_symlinks) + ) + + def copymode(self, dst: str, *, follow_symlinks: bool = True) -> None: + shutil.copymode(self, dst, follow_symlinks=follow_symlinks) - copyfile = shutil.copyfile - copymode = shutil.copymode - copystat = shutil.copystat - copy = shutil.copy - copy2 = shutil.copy2 - copytree = shutil.copytree - move = shutil.move - rmtree = shutil.rmtree + def copystat(self, dst: str, *, follow_symlinks: bool = True) -> None: + shutil.copystat(self, dst, follow_symlinks=follow_symlinks) + + def copy(self, dst: str, *, follow_symlinks: bool = True) -> Self: + return self._next_class(shutil.copy(self, dst, follow_symlinks=follow_symlinks)) + + def copy2(self, dst: str, *, follow_symlinks: bool = True) -> Self: + return self._next_class( + shutil.copy2(self, dst, follow_symlinks=follow_symlinks) + ) + + def copytree( + self, + dst: str, + symlinks: bool = False, + ignore: None | Callable[[str, list[str]], Iterable[str]] = None, + copy_function: _CopyFn = shutil.copy2, + ignore_dangling_symlinks: bool = False, + dirs_exist_ok: bool = False, + ) -> Self: + return self._next_class( + shutil.copytree( + self, + dst, + symlinks=symlinks, + ignore=ignore, + copy_function=copy_function, + ignore_dangling_symlinks=ignore_dangling_symlinks, + dirs_exist_ok=dirs_exist_ok, + ) + ) + + def move(self, dst: str, copy_function: _CopyFn = shutil.copy2) -> Self: + retval = shutil.move(self, dst, copy_function=copy_function) + # shutil.move may return None if the src and dst are the same + return self._next_class(retval or dst) + + if sys.version_info >= (3, 12): + + @overload + def rmtree( + self, + ignore_errors: bool, + onerror: _OnErrorCallback, + *, + onexc: None = ..., + dir_fd: int | None = ..., + ) -> None: ... + @overload + def rmtree( + self, + ignore_errors: bool = ..., + *, + onerror: _OnErrorCallback, + onexc: None = ..., + dir_fd: int | None = ..., + ) -> None: ... + @overload + def rmtree( + self, + ignore_errors: bool = ..., + *, + onexc: _OnExcCallback | None = ..., + dir_fd: int | None = ..., + ) -> None: ... + + elif sys.version_info >= (3, 11): + # NOTE: Strictly speaking, an overload is not needed - this could + # be expressed in a single annotation. However, if overloads + # are used there must be a minimum of two, so this was split + # into two so that the body of rmtree need not be re-defined + # for each version. + @overload + def rmtree( + self, + onerror: _OnErrorCallback | None = None, + *, + dir_fd: int | None = None, + ) -> None: ... + @overload + def rmtree( + self, + ignore_errors: bool, + onerror: _OnErrorCallback | None = ..., + *, + dir_fd: int | None = ..., + ) -> None: ... + + else: + # NOTE: See note about overloads above. + @overload + def rmtree(self, onerror: _OnErrorCallback | None = ...) -> None: ... + @overload + def rmtree( + self, ignore_errors: bool, onerror: _OnErrorCallback | None = ... + ) -> None: ... + + def rmtree(self, *args, **kwargs): + shutil.rmtree(self, *args, **kwargs) + + # Copy the docstrings from shutil to these methods. + + copyfile.__doc__ = shutil.copyfile.__doc__ + copymode.__doc__ = shutil.copymode.__doc__ + copystat.__doc__ = shutil.copystat.__doc__ + copy.__doc__ = shutil.copy.__doc__ + copy2.__doc__ = shutil.copy2.__doc__ + copytree.__doc__ = shutil.copytree.__doc__ + move.__doc__ = shutil.move.__doc__ + rmtree.__doc__ = shutil.rmtree.__doc__ def rmtree_p(self) -> Self: """Like :meth:`rmtree`, but does not raise an exception if the @@ -1527,7 +1639,7 @@ def merge_tree( dst: str, symlinks: bool = False, *, - copy_function: Callable[[str, str], None] = shutil.copy2, + copy_function: _CopyFn = shutil.copy2, ignore: Callable[[Any, list[str]], list[str] | set[str]] = lambda dir, contents: [], ): From 58c650eb1b59966339f2e4de6f25bc7a2ef48a6e Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 12:58:00 -0800 Subject: [PATCH 790/835] Ignore unknown types for win32security --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index f98b8fcf..afbf9a6d 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -51,7 +51,7 @@ from types import ModuleType with contextlib.suppress(ImportError): - import win32security + import win32security # type: ignore[import-not-found] with contextlib.suppress(ImportError): import pwd From 26819d2be271a78b2b5ef7010e0230ba766f6ab2 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 13:04:01 -0800 Subject: [PATCH 791/835] Correct the startfile annotation --- path/__init__.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index afbf9a6d..854d7ab6 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1692,10 +1692,34 @@ def chroot(self) -> None: # pragma: nocover os.chroot(self) if sys.platform == "win32": + if sys.version_info >= (3, 10): - def startfile(self, operation: str | None = None) -> Self: # pragma: nocover + @overload + def startfile( + self, + arguments: str = ..., + cwd: str | None = ..., + show_cmd: int = ..., + ) -> Self: ... + @overload + def startfile( + self, + operation: str, + arguments: str = ..., + cwd: str | None = ..., + show_cmd: int = ..., + ) -> Self: ... + + else: + + @overload + def startfile(self) -> Self: ... + @overload + def startfile(self, operation: str) -> Self: ... + + def startfile(self, *args, **kwargs) -> Self: # pragma: nocover """.. seealso:: :func:`os.startfile`""" - os.startfile(self, operation=operation) + os.startfile(self, *args, **kwargs) return self # in-place re-writing, courtesy of Martijn Pieters From d7e6e2085872142060c80d286df721d479e3d7fa Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 13:21:57 -0800 Subject: [PATCH 792/835] Add some clean-up annotations Added annotations to functions that did not strictly need them but adding them improves editor highlighing. --- path/__init__.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 854d7ab6..ef33c5b4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -84,6 +84,7 @@ _Match = str | Callable[[str], bool] | None _CopyFn = Callable[[str, str], object] + _IgnoreFn = Callable[[str, list[str]], Iterable[str]] _OnErrorCallback = Callable[[Callable[..., Any], str, ExcInfo], object] _OnExcCallback = Callable[[Callable[..., Any], str, BaseException], object] @@ -174,13 +175,13 @@ class Path(str): the Path instance. """ - module: Any = os.path + module: ModuleType = os.path """ The path module to use for path operations. .. seealso:: :mod:`os.path` """ - def __new__(cls, other='.'): + def __new__(cls, other: Any = '.') -> Self: return super().__new__(cls, other) def __init__(self, other: Any = '.') -> None: @@ -312,7 +313,7 @@ def stem(self) -> str: >>> Path('/home/guido/python.tar.gz').stem 'python.tar' """ - base, ext = self.module.splitext(self.name) + base, _ = self.module.splitext(self.name) return base def with_stem(self, stem: str) -> Self: @@ -326,7 +327,7 @@ def with_stem(self, stem: str) -> Self: @property def suffix(self) -> Self: """The file extension, for example ``'.py'``.""" - f, suffix = self.module.splitext(self) + _, suffix = self.module.splitext(self) return suffix def with_suffix(self, suffix: str) -> Self: @@ -345,7 +346,6 @@ def with_suffix(self, suffix: str) -> Self: """ if not suffix.startswith('.'): raise ValueError(f"Invalid suffix {suffix!r}") - return self.stripext() + suffix @property @@ -354,7 +354,7 @@ def drive(self) -> Self: This is always empty on systems that don't use drive specifiers. """ - drive, r = self.module.splitdrive(self) + drive, _ = self.module.splitdrive(self) return self._next_class(drive) @property @@ -1310,10 +1310,10 @@ def chown(self, uid: str | int = -1, gid: str | int = -1) -> Self: .. seealso:: :func:`os.chown` """ - def resolve_uid(uid): + def resolve_uid(uid: str | int) -> int: return uid if isinstance(uid, int) else pwd.getpwnam(uid).pw_uid - def resolve_gid(gid): + def resolve_gid(gid: str | int) -> int: return gid if isinstance(gid, int) else grp.getgrnam(gid).gr_gid os.chown(self, resolve_uid(uid), resolve_gid(gid)) @@ -1525,7 +1525,7 @@ def copytree( self, dst: str, symlinks: bool = False, - ignore: None | Callable[[str, list[str]], Iterable[str]] = None, + ignore: _IgnoreFn | None = None, copy_function: _CopyFn = shutil.copy2, ignore_dangling_symlinks: bool = False, dirs_exist_ok: bool = False, @@ -1640,8 +1640,7 @@ def merge_tree( symlinks: bool = False, *, copy_function: _CopyFn = shutil.copy2, - ignore: Callable[[Any, list[str]], list[str] | set[str]] = lambda dir, - contents: [], + ignore: _IgnoreFn = lambda dir, contents: [], ): """ Copy entire contents of self to dst, overwriting existing @@ -1660,9 +1659,9 @@ def merge_tree( dst_path.makedirs_p() sources = list(self.iterdir()) - _ignored = ignore(self, [item.name for item in sources]) + _ignored = set(ignore(self, [item.name for item in sources])) - def ignored(item): + def ignored(item: Self) -> bool: return item.name in _ignored for source in itertools.filterfalse(ignored, sources): @@ -1844,18 +1843,20 @@ def translate() -> Iterator[None]: raise -def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: +def only_newer(copy_func: _CopyFn) -> _CopyFn: """ Wrap a copy function (like shutil.copy2) to return the dst if it's newer than the source. """ @functools.wraps(copy_func) - def wrapper(src, dst, *args, **kwargs): - is_newer_dst = dst.exists() and dst.getmtime() >= src.getmtime() + def wrapper(src: str, dst: str): + src_p = Path(src) + dst_p = Path(dst) + is_newer_dst = dst_p.exists() and dst_p.getmtime() >= src_p.getmtime() if is_newer_dst: return dst - return copy_func(src, dst, *args, **kwargs) + return copy_func(src, dst) return wrapper From 77b8af35f196d22cede8ac15c5fba74b2c996fa6 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 13:29:55 -0800 Subject: [PATCH 793/835] Fix type hinting for .joinpath Type hinting was taking its cue from the multimethod decorator, so joinpath was always returning Any as the type. To resolve, the multimethod decorator has been annotated with generics so that the typing system can correctly infer the return type of joinpath. --- path/classes.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/path/classes.py b/path/classes.py index 2914996b..7aeb7f2f 100644 --- a/path/classes.py +++ b/path/classes.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import functools -from typing import Any, Callable +from typing import Any, Callable, Generic, TypeVar class ClassProperty(property): @@ -8,18 +10,21 @@ def __get__(self, cls: Any, owner: type | None = None) -> Any: return self.fget.__get__(None, owner)() -class multimethod: +_T = TypeVar("_T") + + +class multimethod(Generic[_T]): """ Acts like a classmethod when invoked from the class and like an instancemethod when invoked from the instance. """ - func: Callable[..., Any] + func: Callable[..., _T] - def __init__(self, func: Callable[..., Any]): + def __init__(self, func: Callable[..., _T]): self.func = func - def __get__(self, instance: Any | None, owner: type | None) -> Any: + def __get__(self, instance: _T | None, owner: type[_T] | None) -> Callable[..., _T]: """ If called on an instance, pass the instance as the first argument. From 0f700b68c3dbca7a271d4afd82f06b76cdc0507a Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 13:43:10 -0800 Subject: [PATCH 794/835] Account for Python 3.9 when defining unions The union for _Match is not defined as part of a function annotation, so the "annotations" import from __future__ does not take effect and thus results in a syntax error on Python < 3.10. To fix, the Union annotation is used for this one type definition. --- path/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index ef33c5b4..61f8ed44 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -72,7 +72,7 @@ ) if TYPE_CHECKING: - from typing_extensions import Literal, Never, Self + from typing_extensions import Literal, Never, Self, Union from _typeshed import ( OpenBinaryMode, OpenBinaryModeReading, @@ -82,7 +82,7 @@ ExcInfo, ) - _Match = str | Callable[[str], bool] | None + _Match = Union[str, Callable[[str], bool], None] _CopyFn = Callable[[str, str], object] _IgnoreFn = Callable[[str, list[str]], Iterable[str]] _OnErrorCallback = Callable[[Callable[..., Any], str, ExcInfo], object] From d63dc73ae2b1b28443d00ad754ea5721e281df88 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Mon, 23 Dec 2024 14:12:51 -0800 Subject: [PATCH 795/835] Directly implement get_owner The implementations for get_owner need to be hiddent behind sys.platform in order for mypy to not complain about non-existing functionality on some platforms. --- path/__init__.py | 57 ++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index 61f8ed44..d99bfa5e 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1188,43 +1188,38 @@ def lstat(self) -> os.stat_result: """ return os.lstat(self) - def __get_owner_windows(self) -> str: # pragma: nocover - r""" - Return the name of the owner of this file or directory. Follow - symbolic links. + if sys.platform == "win32": - Return a name of the form ``DOMAIN\User Name``; may be a group. + def get_owner(self) -> str: # pragma: nocover + r""" + Return the name of the owner of this file or directory. Follow + symbolic links. - .. seealso:: :attr:`owner` - """ - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION - ) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + '\\' + account + Return a name of the form ``DOMAIN\User Name``; may be a group. - def __get_owner_unix(self) -> str: # pragma: nocover - """ - Return the name of the owner of this file or directory. Follow - symbolic links. - - .. seealso:: :attr:`owner` - """ - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name + .. seealso:: :attr:`owner` + """ + if "win32security" not in globals(): + raise NotImplementedError("Ownership not available on this platform.") - def __get_owner_not_implemented(self) -> Never: # pragma: nocover - raise NotImplementedError("Ownership not available on this platform.") + desc = win32security.GetFileSecurity( + self, win32security.OWNER_SECURITY_INFORMATION + ) + sid = desc.GetSecurityDescriptorOwner() + account, domain, typecode = win32security.LookupAccountSid(None, sid) + return domain + '\\' + account - if sys.platform != "win32": - get_owner = __get_owner_unix else: - get_owner = ( - __get_owner_windows - if "win32security" in globals() - else __get_owner_not_implemented - ) + + def get_owner(self) -> str: # pragma: nocover + """ + Return the name of the owner of this file or directory. Follow + symbolic links. + + .. seealso:: :attr:`owner` + """ + st = self.stat() + return pwd.getpwuid(st.st_uid).pw_name @property def owner(self) -> str: From 27c406b8b0a5bdc0e2b4749e1d2ae407e806b3c8 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Wed, 25 Dec 2024 16:14:41 -0500 Subject: [PATCH 796/835] Limit docs analysis warnings for newly added types --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 911c513e..fe85d77b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,6 +49,13 @@ ('py:class', 'OpenBinaryModeUpdating'), ('py:class', 'OpenBinaryModeWriting'), ('py:class', 'OpenTextMode'), + ('py:class', '_IgnoreFn'), + ('py:class', '_CopyFn'), + ('py:class', '_Match'), + ('py:class', '_OnErrorCallback'), + ('py:class', '_OnExcCallback'), + ('py:class', 'os.statvfs_result'), + ('py:class', 'ModuleType'), ] # Include Python intersphinx mapping to prevent failures From 593905fdae5e9698092f78ec11374e61056b87ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 28 Dec 2024 11:01:04 -0500 Subject: [PATCH 797/835] Add news fragment. --- newsfragments/235.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/235.feature.rst diff --git a/newsfragments/235.feature.rst b/newsfragments/235.feature.rst new file mode 100644 index 00000000..7cbc1bc8 --- /dev/null +++ b/newsfragments/235.feature.rst @@ -0,0 +1 @@ +Fully inlined the type annotations. Big thanks to SethMMorton. From 229d04f07dde66204c28ea59127d7c376206fc31 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 28 Dec 2024 11:01:21 -0500 Subject: [PATCH 798/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/235.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/235.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 93022d4f..1dcfdebb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v17.1.0 +======= + +Features +-------- + +- Fully inlined the type annotations. Big thanks to SethMMorton. (#235) + + v17.0.0 ======= diff --git a/newsfragments/235.feature.rst b/newsfragments/235.feature.rst deleted file mode 100644 index 7cbc1bc8..00000000 --- a/newsfragments/235.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Fully inlined the type annotations. Big thanks to SethMMorton. From efa71fcb34e5a9d34b34474326af67d082ad9b4a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 28 Dec 2024 11:22:33 -0500 Subject: [PATCH 799/835] Add 'Traversal' to __all__ so it shows up in docs. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index d99bfa5e..d05891ca 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -92,7 +92,7 @@ from . import classes, masks, matchers from .compat.py38 import removeprefix, removesuffix -__all__ = ['Path', 'TempDir'] +__all__ = ['Path', 'TempDir', 'Traversal'] LINESEPS = ['\r\n', '\r', '\n'] U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029'] From 39a607d25def76ef760334a494554847da8c8f0f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:13 -0500 Subject: [PATCH 800/835] Bump badge for 2025. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index efabeee4..4d3cabee 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2024-informational +.. image:: https://img.shields.io/badge/skeleton-2025-informational :target: https://blog.jaraco.com/skeleton From c7916e237d1c729f01737665a59130a5581968fa Mon Sep 17 00:00:00 2001 From: Andreas Niederl Date: Thu, 16 Jan 2025 00:51:56 +0100 Subject: [PATCH 801/835] add tests for TempDir constructor arguments --- tests/test_path.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_path.py b/tests/test_path.py index 724dbeb8..a7dba880 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -25,6 +25,7 @@ import stat import subprocess import sys +import tempfile import textwrap import time import types @@ -1063,6 +1064,30 @@ def test_cleaned_up_on_interrupt(self): assert not d.exists() + def test_constructor_dir_argument(self): + """ + It should be possible to provide a dir argument to the constructor + """ + base = tempfile.mkdtemp() + with TempDir(dir=base) as d: + assert str(d).startswith(base) + + def test_constructor_prefix_argument(self): + """ + It should be possible to provide a prefix argument to the constructor + """ + prefix = 'test_prefix' + with TempDir(prefix=prefix) as d: + assert d.name.startswith(prefix) + + def test_constructor_suffix_argument(self): + """ + It should be possible to provide a suffix argument to the constructor + """ + suffix = 'test_suffix' + with TempDir(suffix=suffix) as d: + assert str(d).endswith(suffix) + class TestUnicode: @pytest.fixture(autouse=True) From 1d695354c9cbb7cc702fe9ccd88facc2466f55ec Mon Sep 17 00:00:00 2001 From: Andreas Niederl Date: Thu, 16 Jan 2025 00:52:20 +0100 Subject: [PATCH 802/835] re-insert deleted initializer arguments for TempDir --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index d05891ca..4c7cf48b 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1994,7 +1994,7 @@ def __new__(cls, *args, **kwargs) -> Self: dirname = tempfile.mkdtemp(*args, **kwargs) return super().__new__(cls, dirname) - def __init__(self) -> None: + def __init__(self, *args, **kwargs) -> None: pass def __enter__(self) -> Self: From 797fa31a138e7cdc9ae34b30dbfe524de51871ce Mon Sep 17 00:00:00 2001 From: Andreas Niederl Date: Thu, 16 Jan 2025 19:11:38 +0100 Subject: [PATCH 803/835] use tmpdir fixture instead of mkdtemp() --- tests/test_path.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_path.py b/tests/test_path.py index a7dba880..b424bb6b 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -25,7 +25,6 @@ import stat import subprocess import sys -import tempfile import textwrap import time import types @@ -1064,13 +1063,12 @@ def test_cleaned_up_on_interrupt(self): assert not d.exists() - def test_constructor_dir_argument(self): + def test_constructor_dir_argument(self, tmpdir): """ It should be possible to provide a dir argument to the constructor """ - base = tempfile.mkdtemp() - with TempDir(dir=base) as d: - assert str(d).startswith(base) + with TempDir(dir=tmpdir) as d: + assert str(d).startswith(str(tmpdir)) def test_constructor_prefix_argument(self): """ From aee344d781920bba42ddbee4b4b44af29d7bab6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Feb 2025 10:44:24 -0500 Subject: [PATCH 804/835] Removing dependabot config. Closes jaraco/skeleton#156 --- .github/dependabot.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 89ff3396..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-type: "all" From 75ce9aba3ed9f4002fa01db0287dfdb1600fb635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Feb 2025 18:57:40 -0500 Subject: [PATCH 805/835] Add support for building lxml on pre-release Pythons. Closes jaraco/skeleton#161 --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c01fc4d..5841cc37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,13 @@ jobs: continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 + - name: Install build dependencies + # Install dependencies for building packages on pre-release Pythons + # jaraco/skeleton#161 + if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' + run: | + sudo apt update + sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python uses: actions/setup-python@v4 with: From 1c9467fdec1cc1456772cd71c7e740f048ce86fc Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 24 Feb 2025 22:00:11 +0000 Subject: [PATCH 806/835] Fix new mandatory configuration field for RTD (jaraco/skeleton#159) This field is now required and prevents the build from running if absent. Details in https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/ --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dc8516ac..72437063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,9 @@ python: extra_requirements: - doc +sphinx: + configuration: docs/conf.py + # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-lts-latest From 1a2f93053d789f041d88c97c5da4eea9e949bdfe Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 25 Feb 2025 13:21:13 -0500 Subject: [PATCH 807/835] Select Ruff rules for modern type annotations (jaraco/skeleton#160) * Select Ruff rules for modern type annotations Ensure modern type annotation syntax and best practices Not including those covered by type-checkers or exclusive to Python 3.11+ Not including rules currently in preview either. These are the same set of rules I have in pywin32 as of https://github.com/mhammond/pywin32/pull/2458 setuptools has all the same rules enabled (except it also includes the `UP` group directly) * Add PYI011 ignore and #local section * Update ruff.toml Co-authored-by: Jason R. Coombs * Add # upstream --------- Co-authored-by: Jason R. Coombs --- ruff.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ruff.toml b/ruff.toml index 9379d6e1..1d65c7c2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,11 +3,32 @@ extend = "pyproject.toml" [lint] extend-select = [ + # upstream + "C901", "PERF401", "W", + + # Ensure modern type annotation syntax and best practices + # Not including those covered by type-checkers or exclusive to Python 3.11+ + "FA", # flake8-future-annotations + "F404", # late-future-import + "PYI", # flake8-pyi + "UP006", # non-pep585-annotation + "UP007", # non-pep604-annotation + "UP010", # unnecessary-future-import + "UP035", # deprecated-import + "UP037", # quoted-annotation + "UP043", # unnecessary-default-type-args + + # local ] ignore = [ + # upstream + + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, + # irrelevant to this project. + "PYI011", # typed-argument-default-in-stub # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", @@ -23,6 +44,8 @@ ignore = [ "COM819", "ISC001", "ISC002", + + # local ] [format] From aa891069099398fe2eb294ac4b781460d8c0a39b Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 26 Feb 2025 17:56:42 -0500 Subject: [PATCH 808/835] Consistent import sorting (isort) (jaraco/skeleton#157) --- ruff.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 1d65c7c2..b52a6d7c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,9 +5,10 @@ extend = "pyproject.toml" extend-select = [ # upstream - "C901", - "PERF401", - "W", + "C901", # complex-structure + "I", # isort + "PERF401", # manual-list-comprehension + "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From 8f42595ca65133aeb4b75f38183233c27b2e6247 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:19:07 +0100 Subject: [PATCH 809/835] Enable ruff rules ISC001/ISC002 (jaraco/skeleton#158) Starting with ruff 0.9.1, they are compatible with the ruff formatter when used together. --- ruff.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index b52a6d7c..2b679267 100644 --- a/ruff.toml +++ b/ruff.toml @@ -43,8 +43,6 @@ ignore = [ "Q003", "COM812", "COM819", - "ISC001", - "ISC002", # local ] From b7d4b6ee00804bef36a8c398676e207813540c3b Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 4 Mar 2025 03:24:14 -0500 Subject: [PATCH 810/835] remove extra spaces in ruff.toml (jaraco/skeleton#164) --- ruff.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index 2b679267..1e952846 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,13 +4,13 @@ extend = "pyproject.toml" [lint] extend-select = [ # upstream - + "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension "W", # pycodestyle Warning - - # Ensure modern type annotation syntax and best practices + + # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ "FA", # flake8-future-annotations "F404", # late-future-import @@ -26,7 +26,7 @@ extend-select = [ ] ignore = [ # upstream - + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, # irrelevant to this project. "PYI011", # typed-argument-default-in-stub @@ -44,7 +44,7 @@ ignore = [ "COM812", "COM819", - # local + # local ] [format] From b00e9dd730423a399c1d3c3d5621687adff0c5a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Mar 2025 09:05:55 -0500 Subject: [PATCH 811/835] Remove pycodestyle warnings, no longer meaningful when using ruff formatter. Ref https://github.com/jaraco/skeleton/commit/d1c5444126aeacefee3949b30136446ab99979d8#commitcomment-153409678 --- ruff.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 1e952846..267a1ba1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -8,7 +8,6 @@ extend-select = [ "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension - "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From d587ff737ee89778cf6f4bbd249e770c965fee06 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:08:11 +0100 Subject: [PATCH 812/835] Update to the latest ruff version (jaraco/skeleton#166) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04870d16..633e3648 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.9.9 hooks: - id: ruff args: [--fix, --unsafe-fixes] From ad84110008b826efd6e39bcc39b9998b4f1cc767 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 21 Mar 2025 00:14:38 +0000 Subject: [PATCH 813/835] Remove deprecated license classifier (PEP 639) (jaraco/skeleton#170) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 328b98cb..71b1a7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] From 1ebb559a507f97ece7342d7f1532a49188cade33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Mar 2025 20:56:31 -0400 Subject: [PATCH 814/835] Remove workaround and update badge. Closes jaraco/skeleton#155 --- README.rst | 2 +- ruff.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4d3cabee..3000f5ab 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests -.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/ruff.toml b/ruff.toml index 267a1ba1..63c0825f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,3 @@ -# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -extend = "pyproject.toml" - [lint] extend-select = [ # upstream From 979e626055ab60095b37be04555a01a40f62e470 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Mar 2025 05:33:58 -0400 Subject: [PATCH 815/835] Remove PIP_NO_PYTHON_VERSION_WARNING. Ref pypa/pip#13154 --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5841cc37..928acf2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,6 @@ env: # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' - PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Ensure tests can sense settings about the environment From 9a81db3c77bc106017dcd4b0853a5a94f43ae33c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 May 2025 03:57:47 -0400 Subject: [PATCH 816/835] Replace copy of license with an SPDX identifier. (jaraco/skeleton#171) --- LICENSE | 17 ----------------- pyproject.toml | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1bb5a443..00000000 --- a/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 71b1a7da..fa0c801f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] requires-python = ">=3.9" +license = "MIT" dependencies = [ ] dynamic = ["version"] From 867396152fcb99055795120750dfda53f85bb414 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 4 May 2025 22:06:52 +0200 Subject: [PATCH 817/835] Python 3 is the default nowadays (jaraco/skeleton#173) --- .github/workflows/main.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 928acf2c..80294970 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,7 +63,7 @@ jobs: sudo apt update sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -85,9 +85,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -119,9 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Run From d2b8d7750f78e870def98c4e04053af4acc86e29 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 May 2025 12:32:22 -0400 Subject: [PATCH 818/835] Add coherent.licensed plugin to inject license texts into the build. Closes jaraco/skeleton#174 --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa0c801f..bda001a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,10 @@ [build-system] -requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] +requires = [ + "setuptools>=61.2", + "setuptools_scm[toml]>=3.4.1", + # jaraco/skeleton#174 + "coherent.licensed", +] build-backend = "setuptools.build_meta" [project] From b535e75e95389eb8a16e34b238e2483f498593c8 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 10 May 2025 18:47:43 +0200 Subject: [PATCH 819/835] Revert "Python 3 is the default nowadays (jaraco/skeleton#173)" (jaraco/skeleton#175) This reverts commit 867396152fcb99055795120750dfda53f85bb414. Removing `python-version` falls back on the Python bundled with the runner, making actions/setup-python a no-op. Here, the maintainer prefers using the latest release of Python 3. This is what `3.x` means: use the latest release of Python 3. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80294970..53513eee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,6 +86,8 @@ jobs: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -118,6 +120,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Run From 5a6c1532c206871bc2913349d97dda06e01b9963 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 May 2025 23:20:37 -0400 Subject: [PATCH 820/835] Bump to setuptools 77 or later. Closes jaraco/skeleton#176 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bda001a4..ce6c1709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools>=61.2", + "setuptools>=77", "setuptools_scm[toml]>=3.4.1", # jaraco/skeleton#174 "coherent.licensed", From 04ff5549ee93f907bcebb1db570ad291ae55fd29 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 22 Jun 2025 13:49:02 +0100 Subject: [PATCH 821/835] Update pre-commit ruff (jaraco/skeleton#181) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 633e3648..fa559241 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.12.0 hooks: - id: ruff args: [--fix, --unsafe-fixes] From 8c5810ed39f431598f8498499e7e8fa38a8ed455 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 22 Jun 2025 08:50:30 -0400 Subject: [PATCH 822/835] Log filenames when running pytest-mypy (jaraco/skeleton#177) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce6c1709..e916f46b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,12 +58,12 @@ cover = [ ] enabler = [ - "pytest-enabler >= 2.2", + "pytest-enabler >= 3.4", ] type = [ # upstream - "pytest-mypy", + "pytest-mypy >= 1.0.1", # local ] From 5357ae1697a5ee27ca5b5e1041f19bef8329c406 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jul 2025 16:09:40 -0400 Subject: [PATCH 823/835] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- path/__init__.py | 12 ++++++------ path/masks.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index d05891ca..f1f4be49 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -59,28 +59,28 @@ with contextlib.suppress(ImportError): import grp +from collections.abc import Generator, Iterable, Iterator from typing import ( - TYPE_CHECKING, IO, + TYPE_CHECKING, Any, BinaryIO, Callable, - Generator, - Iterable, - Iterator, overload, ) if TYPE_CHECKING: - from typing_extensions import Literal, Never, Self, Union + from typing import Union + from _typeshed import ( + ExcInfo, OpenBinaryMode, OpenBinaryModeReading, OpenBinaryModeUpdating, OpenBinaryModeWriting, OpenTextMode, - ExcInfo, ) + from typing_extensions import Literal, Never, Self _Match = Union[str, Callable[[str], bool], None] _CopyFn = Callable[[str, str], object] diff --git a/path/masks.py b/path/masks.py index 2c727bee..6c551d71 100644 --- a/path/masks.py +++ b/path/masks.py @@ -4,8 +4,8 @@ import itertools import operator import re - -from typing import Any, Callable, Iterable, Iterator +from collections.abc import Iterable, Iterator +from typing import Any, Callable # from jaraco.functools From 04205163cd8cc3b013cb43b7bb32c5668f8429d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jul 2025 16:16:22 -0400 Subject: [PATCH 824/835] Add news fragment. --- newsfragments/236.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/236.bugfix.rst diff --git a/newsfragments/236.bugfix.rst b/newsfragments/236.bugfix.rst new file mode 100644 index 00000000..8882116c --- /dev/null +++ b/newsfragments/236.bugfix.rst @@ -0,0 +1 @@ +Fixed TempDir constructor arguments. From 8326ac9e50dc97af63ad701c02f963dc3edb3139 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jul 2025 16:18:34 -0400 Subject: [PATCH 825/835] Supply the types, irrespective of platform. Closes #239 --- path/__init__.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index c12416c0..c1aa9797 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -51,7 +51,7 @@ from types import ModuleType with contextlib.suppress(ImportError): - import win32security # type: ignore[import-not-found] + import win32security with contextlib.suppress(ImportError): import pwd diff --git a/pyproject.toml b/pyproject.toml index 36b087b4..88fa87b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ test = [ "more_itertools", # required for checkdocs on README.rst "pygments", + "types-pywin32", ] doc = [ From fdd8436cdeb2903b4fc7ce67dbbd549a69c77d88 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jul 2025 16:19:05 -0400 Subject: [PATCH 826/835] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/236.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/236.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 1dcfdebb..93e57e9a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v17.1.1 +======= + +Bugfixes +-------- + +- Fixed TempDir constructor arguments. (#236) + + v17.1.0 ======= diff --git a/newsfragments/236.bugfix.rst b/newsfragments/236.bugfix.rst deleted file mode 100644 index 8882116c..00000000 --- a/newsfragments/236.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed TempDir constructor arguments. From 0f86209f11199ceaff788c73f96ea5b5ec2318ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jul 2025 16:35:09 -0400 Subject: [PATCH 827/835] Ignore the arg type. Workaround for python/typeshed#14483. --- path/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/path/__init__.py b/path/__init__.py index c1aa9797..33dd979c 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -1206,7 +1206,7 @@ def get_owner(self) -> str: # pragma: nocover self, win32security.OWNER_SECURITY_INFORMATION ) sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) + account, domain, typecode = win32security.LookupAccountSid(None, sid) # type: ignore[arg-type] return domain + '\\' + account else: From d47a969ed4567bbdee26034ccaaa8b8169f44fcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Oct 2025 13:06:02 -0400 Subject: [PATCH 828/835] Specify the directory for news fragments. Uses the default as found on towncrier prior to 25 and sets to a predictable value. Fixes jaraco/skeleton#184 --- towncrier.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/towncrier.toml b/towncrier.toml index 6fa480e4..577e87a7 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,2 +1,3 @@ [tool.towncrier] title_format = "{version}" +directory = "newsfragments" # jaraco/skeleton#184 From 8f3d95e7db0114e26e57dd95932b141ead74f7c5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:04:12 -0500 Subject: [PATCH 829/835] Pin mypy on PyPy. Closes jaraco/skeleton#188. Ref python/mypy#20454. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e916f46b..987b802c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,9 @@ type = [ # upstream "pytest-mypy >= 1.0.1", + ## workaround for python/mypy#20454 + "mypy < 1.19; python_implementation == 'PyPy'", + # local ] From d5c6862e8d8291aec83aeea8261191e491a63d68 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:48:11 +0200 Subject: [PATCH 830/835] Update pre-commit ruff legacy alias (jaraco/skeleton#183) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa559241..54cc8303 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: [--fix, --unsafe-fixes] - id: ruff-format From d9b029be3925b99d3b0d2ef529d79d0a1b9d2c52 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 13 Mar 2026 10:56:44 -0400 Subject: [PATCH 831/835] Don't install (nor run) mypy on PyPy (librt build failures) (jaraco/skeleton#187) --------- Co-authored-by: Jason R. Coombs --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 987b802c..cdf82cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,10 +63,9 @@ enabler = [ type = [ # upstream - "pytest-mypy >= 1.0.1", - - ## workaround for python/mypy#20454 - "mypy < 1.19; python_implementation == 'PyPy'", + + # Exclude PyPy from type checks (python/mypy#20454 jaraco/skeleton#187) + "pytest-mypy >= 1.0.1; platform_python_implementation != 'PyPy'", # local ] From 16fb289d38af0d510e39afcbbd43bace2d6d8dd9 Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 13 Mar 2026 10:59:27 -0400 Subject: [PATCH 832/835] Bump `pytest-checkdocs` to `>= 2.14` to resolve deprecation warnings (jaraco/skeleton#189) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdf82cfb..5b2a8a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ doc = [ ] check = [ - "pytest-checkdocs >= 2.4", + "pytest-checkdocs >= 2.14", "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", ] From 07389c4c4609a49826ea9ed510419c2e32eccee9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:18:41 -0400 Subject: [PATCH 833/835] Bump Python versions: drop 3.9 (EOL), add 3.15 (jaraco/skeleton#193) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jaraco <308610+jaraco@users.noreply.github.com> --- .github/workflows/main.yml | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53513eee..d40c74ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,31 +34,31 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.9" + - "3.10" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.10" - platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest - python: "3.12" platform: ubuntu-latest - python: "3.14" platform: ubuntu-latest + - python: "3.15" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.14' }} + continue-on-error: ${{ matrix.python == '3.15' }} steps: - uses: actions/checkout@v4 - name: Install build dependencies # Install dependencies for building packages on pre-release Pythons # jaraco/skeleton#161 - if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' + if: matrix.python == '3.15' && matrix.platform == 'ubuntu-latest' run: | sudo apt update sudo apt install -y libxml2-dev libxslt-dev diff --git a/pyproject.toml b/pyproject.toml index 5b2a8a82..a25e78ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.9" +requires-python = ">=3.10" license = "MIT" dependencies = [ ] From 606a7a5f999e6a43480015460be604a77f16ce68 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:20:06 +0200 Subject: [PATCH 834/835] Fix CI warning in diffcov report (jaraco/skeleton#194) UserWarning: The --html-report option is deprecated. Use --format html:diffcov.html instead. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 14243051..e05a3d4a 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = diff-cover commands = pytest {posargs} --cov-report xml - diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --format html:diffcov.html diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] From 684a3157eae9988cdd8e95969efa9a79a70f69f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 13 Apr 2026 04:21:33 -0400 Subject: [PATCH 835/835] Bump badge for 2026. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3000f5ab..44f48639 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2025-informational +.. image:: https://img.shields.io/badge/skeleton-2026-informational :target: https://blog.jaraco.com/skeleton