From f3ff0541b6967ee91be3f572c5afe4f559436aa1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 12:12:55 -0500 Subject: [PATCH 001/496] 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 002/496] 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 9edd5b701549833ebbfb354c072962c58e5394ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Mar 2019 19:54:23 -0400 Subject: [PATCH 003/496] 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 e8db26a129378279833620da952a1ecc6cef937b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 May 2019 16:17:33 -0400 Subject: [PATCH 004/496] 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 005/496] 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 fd540b6535f56647581df61333bac7eadc1309f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2019 20:01:38 -0400 Subject: [PATCH 006/496] 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 007/496] 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 008/496] 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 009/496] 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 b10e8186a9305d1f899f0fd7d1319b7b15ed1ecd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 20:33:33 -0500 Subject: [PATCH 010/496] 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 011/496] 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 34d87688eae5d10970f3c8269ac01bcca4ad0229 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Jan 2020 17:23:00 -0500 Subject: [PATCH 012/496] 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 fb8d9c43500c06f6f2286ea7c7ae452d41cce412 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 14 Nov 2020 20:32:18 -0500 Subject: [PATCH 013/496] 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 23820d3927cd7eecd39acf3fcb7feaecb5afedb9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Mar 2020 10:49:27 -0400 Subject: [PATCH 014/496] Add resource reader as proposed by Gregory in https://gitlab.com/python-devs/importlib_resources/-/issues/90#note_307473902 --- importlib_resources/abc.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 4371a77c..e4f1eb3a 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -112,6 +112,34 @@ def name(self) -> str: """ +class ResourceReader: + # Holds the string name of the virtual Python package this is a resource loader for. + # For filesystems, this is effectively a reader for a directory at `fullname.replace('.', '/')` + fullname = None + + def child_readers(self) -> [ResourceReader] + """Obtain an iterable of ResourceReader for available child virtual packages of this one. + + On filesystems, this essentially returns instances corresponding to immediate child directories. + """ + + def resources(self) -> [str] + """Obtain available named resources for this virtual package. + + On filesystems, this essentially returns files in the current directory. + TODO consider returning a special type that exposes an `open()`, etc. + """" + + def open_binary(self, resource) -> File + """Obtain a File-like for a named resource. + + On filesystems, this attempts to open os.path.join(self, resource). + + Attempting to open a non-resource entity (such as a subdirectory) or a missing + resource raises NotAResourceError. + """ + + class TraversableResources(ResourceReader): @abc.abstractmethod def files(self): From 0355a88c68aaf44c855d1e13beea813cb0b2aee7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Mar 2020 10:50:42 -0400 Subject: [PATCH 015/496] Fix syntax --- importlib_resources/abc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index e4f1eb3a..164e0195 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -112,25 +112,25 @@ def name(self) -> str: """ -class ResourceReader: +class SimpleReader: # Holds the string name of the virtual Python package this is a resource loader for. # For filesystems, this is effectively a reader for a directory at `fullname.replace('.', '/')` fullname = None - def child_readers(self) -> [ResourceReader] + def child_readers(self) -> ['SimpleReader']: """Obtain an iterable of ResourceReader for available child virtual packages of this one. On filesystems, this essentially returns instances corresponding to immediate child directories. """ - def resources(self) -> [str] + def resources(self) -> [str]: """Obtain available named resources for this virtual package. On filesystems, this essentially returns files in the current directory. TODO consider returning a special type that exposes an `open()`, etc. - """" + """ - def open_binary(self, resource) -> File + def open_binary(self, resource) -> BinaryIO: """Obtain a File-like for a named resource. On filesystems, this attempts to open os.path.join(self, resource). From 93b1c6d57cca3fe587a574ad09c35530c1e2c691 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Mar 2020 10:56:01 -0400 Subject: [PATCH 016/496] Convert to an ABC --- importlib_resources/abc.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 164e0195..39db2461 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -112,31 +112,31 @@ def name(self) -> str: """ -class SimpleReader: - # Holds the string name of the virtual Python package this is a resource loader for. - # For filesystems, this is effectively a reader for a directory at `fullname.replace('.', '/')` - fullname = None +class SimpleReader(ABC): - def child_readers(self) -> ['SimpleReader']: - """Obtain an iterable of ResourceReader for available child virtual packages of this one. + @abc.abstractproperty + def package(self): + """ + The name of the package for which this reader loads resources. + """ - On filesystems, this essentially returns instances corresponding to immediate child directories. + @abc.abstractmethod + def child_readers(self) -> ['SimpleReader']: + """ + Obtain an iterable of ResourceReader for available + child virtual packages of this one. """ + @abc.abstractmethod def resources(self) -> [str]: - """Obtain available named resources for this virtual package. - - On filesystems, this essentially returns files in the current directory. - TODO consider returning a special type that exposes an `open()`, etc. + """ + Obtain available named resources for this virtual package. """ + @abc.abstractmethod def open_binary(self, resource) -> BinaryIO: - """Obtain a File-like for a named resource. - - On filesystems, this attempts to open os.path.join(self, resource). - - Attempting to open a non-resource entity (such as a subdirectory) or a missing - resource raises NotAResourceError. + """ + Obtain a File-like for a named resource. """ From 2d86bc3b710ebc699c1901d39105100df068883f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 18:58:11 -0500 Subject: [PATCH 017/496] Implement TraversableReader, based on SimpleReader --- importlib_resources/abc.py | 59 +++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 39db2461..2473c749 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,4 +1,6 @@ import abc +import io +import itertools from typing import BinaryIO, Iterable, Text from ._compat import runtime_checkable, Protocol @@ -112,7 +114,7 @@ def name(self) -> str: """ -class SimpleReader(ABC): +class SimpleReader(abc.ABC): @abc.abstractproperty def package(self): @@ -139,6 +141,10 @@ def open_binary(self, resource) -> BinaryIO: Obtain a File-like for a named resource. """ + @property + def name(self): + return self.package.split('.')[-1] + class TraversableResources(ResourceReader): @abc.abstractmethod @@ -156,3 +162,54 @@ def is_resource(self, path): def contents(self): return (item.name for item in self.files().iterdir()) + + +class ResourceHandle(Traversable): + def __init__(self, reader, name): + self.reader = reader + self.name = name + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(*args, **kwargs) + return stream + + +class ResourceContainer(Traversable): + def __init__(self, reader: SimpleReader): + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = ( + ResourceHandle(self, name) + for name in self.resources + ) + dirs = map(ResourceContainer, self.child_readers()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + def joinpath(self, name): + return next( + traversable + for traversable in self.iterdir() + if traversable.name == name) + + +class TraversableReader(TraversableResources, SimpleReader): + def files(self): + return ResourceContainer(self) From 0de43b9721dd9cba3a3cd8e66f4acee90d8ae5b4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Mar 2020 11:22:55 -0400 Subject: [PATCH 018/496] Make read_bytes and read_text and __truediv__ concrete implementations based on other abstract methods. --- importlib_resources/abc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 2473c749..7ccadee5 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -61,17 +61,19 @@ def iterdir(self): Yield Traversable objects in self """ - @abc.abstractmethod def read_bytes(self): """ Read contents of self as bytes """ + with self.open('rb') as strm: + return strm.read() - @abc.abstractmethod def read_text(self, encoding=None): """ - Read contents of self as bytes + Read contents of self as text """ + with self.open(encoding=encoding) as strm: + return strm.read() @abc.abstractmethod def is_dir(self): @@ -91,11 +93,11 @@ def joinpath(self, child): Return Traversable child in self """ - @abc.abstractmethod def __truediv__(self, child): """ Return Traversable child in self """ + return self.joinpath(child) @abc.abstractmethod def open(self, mode='r', *args, **kwargs): From 72feafeb4b90f6ba111eaebef9dacce39180f489 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 18:59:03 -0500 Subject: [PATCH 019/496] Fix typing syntax --- importlib_resources/abc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 7ccadee5..cc398dfc 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,7 +1,7 @@ import abc import io import itertools -from typing import BinaryIO, Iterable, Text +from typing import BinaryIO, Iterable, Text, List from ._compat import runtime_checkable, Protocol @@ -125,14 +125,14 @@ def package(self): """ @abc.abstractmethod - def child_readers(self) -> ['SimpleReader']: + def child_readers(self) -> List['SimpleReader']: """ Obtain an iterable of ResourceReader for available child virtual packages of this one. """ @abc.abstractmethod - def resources(self) -> [str]: + def resources(self) -> List[str]: """ Obtain available named resources for this virtual package. """ From b7ee6fbc41e7737cebfa690b66c39f22a370dc9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 24 Mar 2020 11:34:59 -0400 Subject: [PATCH 020/496] Implement abstract method on ResourceHandle and add docstrings --- importlib_resources/abc.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index cc398dfc..47541b21 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -167,6 +167,10 @@ def contents(self): class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + def __init__(self, reader, name): self.reader = reader self.name = name @@ -183,8 +187,15 @@ def open(self, mode='r', *args, **kwargs): stream = io.TextIOWrapper(*args, **kwargs) return stream + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + def __init__(self, reader: SimpleReader): self.reader = reader From 35297321ab56c9b411bcb38d364ec5b4a9f4c72e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Apr 2020 11:41:26 -0400 Subject: [PATCH 021/496] Update wording on SimpleReader.child_readers. Rename 'child_readers' to simply 'children'. --- importlib_resources/abc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 47541b21..23a7b590 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -125,10 +125,10 @@ def package(self): """ @abc.abstractmethod - def child_readers(self) -> List['SimpleReader']: + def children(self) -> List['SimpleReader']: """ - Obtain an iterable of ResourceReader for available - child virtual packages of this one. + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). """ @abc.abstractmethod @@ -210,7 +210,7 @@ def iterdir(self): ResourceHandle(self, name) for name in self.resources ) - dirs = map(ResourceContainer, self.child_readers()) + dirs = map(ResourceContainer, self.children()) return itertools.chain(files, dirs) def open(self, *args, **kwargs): From 27e9d7c70706c84f0ea019da3a71fe0b893e8b87 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Apr 2020 15:08:37 -0400 Subject: [PATCH 022/496] Add compatibility for Python 2.7 --- importlib_resources/abc.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 23a7b590..af3277e0 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -125,20 +125,23 @@ def package(self): """ @abc.abstractmethod - def children(self) -> List['SimpleReader']: + def children(self): + # type: () -> List['SimpleReader'] """ Obtain an iterable of SimpleReader for available child containers (e.g. directories). """ @abc.abstractmethod - def resources(self) -> List[str]: + def resources(self): + # type: () -> List[str] """ Obtain available named resources for this virtual package. """ @abc.abstractmethod - def open_binary(self, resource) -> BinaryIO: + def open_binary(self, resource): + # type: (str) -> BinaryIO """ Obtain a File-like for a named resource. """ @@ -196,7 +199,8 @@ class ResourceContainer(Traversable): Traversable container for a package's resources via its reader. """ - def __init__(self, reader: SimpleReader): + def __init__(self, reader): + # type: (SimpleReader) -> None self.reader = reader def is_dir(self): From 04b02010cc58f7033588d12290f9576fba7c2ba1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Apr 2020 19:53:11 -0400 Subject: [PATCH 023/496] Add docstrings and type annotations. Fix references to parent and reader between ResourceHandle and ResourceContainer. --- importlib_resources/abc.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index af3277e0..0942f8ab 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -77,12 +77,14 @@ def read_text(self, encoding=None): @abc.abstractmethod def is_dir(self): + # type: () -> bool """ Return True if self is a dir """ @abc.abstractmethod def is_file(self): + # type: () -> bool """ Return True if self is a file """ @@ -117,9 +119,14 @@ def name(self) -> str: class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ @abc.abstractproperty def package(self): + # type: () -> str """ The name of the package for which this reader loads resources. """ @@ -152,6 +159,11 @@ def name(self): class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + @abc.abstractmethod def files(self): """Return a Traversable object for the loaded package.""" @@ -174,8 +186,9 @@ class ResourceHandle(Traversable): Handle to a named resource in a ResourceReader. """ - def __init__(self, reader, name): - self.reader = reader + def __init__(self, parent, name): + # type: (ResourceContainer, str) -> None + self.parent = parent self.name = name def is_file(self): @@ -185,7 +198,7 @@ def is_dir(self): return False def open(self, mode='r', *args, **kwargs): - stream = self.reader.open_binary(self.name) + stream = self.parent.reader.open_binary(self.name) if 'b' not in mode: stream = io.TextIOWrapper(*args, **kwargs) return stream @@ -212,9 +225,9 @@ def is_file(self): def iterdir(self): files = ( ResourceHandle(self, name) - for name in self.resources + for name in self.reader.resources ) - dirs = map(ResourceContainer, self.children()) + dirs = map(ResourceContainer, self.reader.children()) return itertools.chain(files, dirs) def open(self, *args, **kwargs): @@ -228,5 +241,10 @@ def joinpath(self, name): class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ def files(self): return ResourceContainer(self) From 4f486494858d3d916c369a4e43fcc7c64f79b084 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 19:02:30 -0500 Subject: [PATCH 024/496] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_resources/abc.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 0942f8ab..40d61819 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -223,10 +223,7 @@ def is_file(self): return False def iterdir(self): - files = ( - ResourceHandle(self, name) - for name in self.reader.resources - ) + files = (ResourceHandle(self, name) for name in self.reader.resources) dirs = map(ResourceContainer, self.reader.children()) return itertools.chain(files, dirs) @@ -235,9 +232,8 @@ def open(self, *args, **kwargs): def joinpath(self, name): return next( - traversable - for traversable in self.iterdir() - if traversable.name == name) + traversable for traversable in self.iterdir() if traversable.name == name + ) class TraversableReader(TraversableResources, SimpleReader): @@ -246,5 +242,6 @@ class TraversableReader(TraversableResources, SimpleReader): may derive from this class to provide the TraversableResources interface by supplying the SimpleReader interface. """ + def files(self): return ResourceContainer(self) From f60d050afc427ef85e5d77cd63317ededa035cee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 19:02:40 -0500 Subject: [PATCH 025/496] =?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 --- importlib_resources/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 40d61819..205a7d91 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -189,7 +189,7 @@ class ResourceHandle(Traversable): def __init__(self, parent, name): # type: (ResourceContainer, str) -> None self.parent = parent - self.name = name + self.name = name # type: ignore def is_file(self): return True From fbd44f05abf991250513f9bfec7611fbf6f620cd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 19:21:23 -0500 Subject: [PATCH 026/496] Move interfaces for low-level readers to their own module. --- importlib_resources/abc.py | 110 +------------------------------- importlib_resources/simple.py | 116 ++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 109 deletions(-) create mode 100644 importlib_resources/simple.py diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 1b2230f8..56dc8127 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,7 +1,5 @@ import abc -import io -import itertools -from typing import BinaryIO, Iterable, Text, List +from typing import BinaryIO, Iterable, Text from ._compat import runtime_checkable, Protocol @@ -116,46 +114,6 @@ def name(self) -> str: """ -class SimpleReader(abc.ABC): - """ - The minimum, low-level interface required from a resource - provider. - """ - - @abc.abstractproperty - def package(self): - # type: () -> str - """ - The name of the package for which this reader loads resources. - """ - - @abc.abstractmethod - def children(self): - # type: () -> List['SimpleReader'] - """ - Obtain an iterable of SimpleReader for available - child containers (e.g. directories). - """ - - @abc.abstractmethod - def resources(self): - # type: () -> List[str] - """ - Obtain available named resources for this virtual package. - """ - - @abc.abstractmethod - def open_binary(self, resource): - # type: (str) -> BinaryIO - """ - Obtain a File-like for a named resource. - """ - - @property - def name(self): - return self.package.split('.')[-1] - - class TraversableResources(ResourceReader): """ The required interface for providing traversable @@ -177,69 +135,3 @@ def is_resource(self, path): def contents(self): return (item.name for item in self.files().iterdir()) - - -class ResourceHandle(Traversable): - """ - Handle to a named resource in a ResourceReader. - """ - - def __init__(self, parent, name): - # type: (ResourceContainer, str) -> None - self.parent = parent - self.name = name # type: ignore - - def is_file(self): - return True - - def is_dir(self): - return False - - def open(self, mode='r', *args, **kwargs): - stream = self.parent.reader.open_binary(self.name) - if 'b' not in mode: - stream = io.TextIOWrapper(*args, **kwargs) - return stream - - def joinpath(self, name): - raise RuntimeError("Cannot traverse into a resource") - - -class ResourceContainer(Traversable): - """ - Traversable container for a package's resources via its reader. - """ - - def __init__(self, reader): - # type: (SimpleReader) -> None - self.reader = reader - - def is_dir(self): - return True - - def is_file(self): - return False - - def iterdir(self): - files = (ResourceHandle(self, name) for name in self.reader.resources) - dirs = map(ResourceContainer, self.reader.children()) - return itertools.chain(files, dirs) - - def open(self, *args, **kwargs): - raise IsADirectoryError() - - def joinpath(self, name): - return next( - traversable for traversable in self.iterdir() if traversable.name == name - ) - - -class TraversableReader(TraversableResources, SimpleReader): - """ - A TraversableResources based on SimpleReader. Resource providers - may derive from this class to provide the TraversableResources - interface by supplying the SimpleReader interface. - """ - - def files(self): - return ResourceContainer(self) diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py new file mode 100644 index 00000000..da073cbd --- /dev/null +++ b/importlib_resources/simple.py @@ -0,0 +1,116 @@ +""" +Interface adapters for low-level readers. +""" + +import abc +import io +import itertools +from typing import BinaryIO, List + +from .abc import Traversable, TraversableResources + + +class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ + + @abc.abstractproperty + def package(self): + # type: () -> str + """ + The name of the package for which this reader loads resources. + """ + + @abc.abstractmethod + def children(self): + # type: () -> List['SimpleReader'] + """ + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). + """ + + @abc.abstractmethod + def resources(self): + # type: () -> List[str] + """ + Obtain available named resources for this virtual package. + """ + + @abc.abstractmethod + def open_binary(self, resource): + # type: (str) -> BinaryIO + """ + Obtain a File-like for a named resource. + """ + + @property + def name(self): + return self.package.split('.')[-1] + + +class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + + def __init__(self, parent, name): + # type: (ResourceContainer, str) -> None + self.parent = parent + self.name = name # type: ignore + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.parent.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(*args, **kwargs) + return stream + + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + + +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader): + # type: (SimpleReader) -> None + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + def joinpath(self, name): + return next( + traversable for traversable in self.iterdir() if traversable.name == name + ) + + +class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ + + def files(self): + return ResourceContainer(self) From 446699153a9681242e951212aefdc725c0fff08c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Jan 2021 19:23:03 -0500 Subject: [PATCH 027/496] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 995f8391..2d4d6cea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.1.0 +====== + +* Added ``simple`` module implementing adapters from + a low-level resource reader interface to a + ``TraversableResources`` interface. Closes #90. + v5.0.1 ====== From 51298a2cc4faa7253e9fe41d7a9574cf9aac997c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Feb 2021 23:08:58 -0500 Subject: [PATCH 028/496] 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 029/496] 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 38fff62edb5e282f144dc77cc1bf5555367336d9 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Sat, 6 Feb 2021 23:03:13 +0300 Subject: [PATCH 030/496] 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 5e416793c008c5ef285c37828072fbea5ced6d08 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Feb 2021 21:34:35 -0500 Subject: [PATCH 031/496] 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 032/496] 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 58527628e3216923afd32239866b47ae443699df Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Feb 2021 11:17:24 -0500 Subject: [PATCH 033/496] Remove Python version comment, as namespace support is not native on Python 3.9. --- importlib_resources/_compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index bfdd7ff3..421c3bcf 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -47,7 +47,6 @@ def path(self): return self.spec.origin def get_resource_reader(self, name): - # Python < 3.9 from . import readers def _zip_reader(spec): From ba01a3e514c0aa880fab44cee15b7e8acd67c8cf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Feb 2021 14:43:25 -0500 Subject: [PATCH 034/496] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9e883910..b13a9d76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.1.1 +====== + +* Re-release with changes from 5.0.x. + v5.0.3 ====== From d094cac95634795704b710f97716a72d0599747a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Mar 2021 13:36:24 -0500 Subject: [PATCH 035/496] Update changelog. --- CHANGES.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 35c70155..8eddf92d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.1.2 +====== + +* Re-release with changes from 5.0.4. + v5.0.4 ====== @@ -7,7 +12,7 @@ v5.0.4 v5.1.1 ====== -* Re-release with changes from 5.0.x. +* Re-release with changes from 5.0.3. v5.0.3 ====== From bf9fae2c0df316dc837d56ae68880620733d5ff6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Mar 2021 09:57:43 -0500 Subject: [PATCH 036/496] 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 037/496] 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 038/496] 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 039/496] 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 040/496] 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 041/496] 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 042/496] 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 043/496] 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 e05b43aa995a99025803c4a59c25c9b926ad0762 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 28 Mar 2021 00:04:04 +0100 Subject: [PATCH 044/496] Add test for propery MultiplexedPath.name --- importlib_resources/tests/test_reader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 64e04102..4fe5fdd4 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -83,6 +83,12 @@ def test_repr(self): "MultiplexedPath('{}')".format(self.folder), ) + def test_name(self): + self.assertEqual( + MultiplexedPath(self.folder).name, + os.path.basename(self.folder), + ) + class NamespaceReaderTest(unittest.TestCase): site_dir = str(pathlib.Path(__file__).parent) From 9822338e8de57e99aa3c26275ebdbf8925af7bc8 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 28 Mar 2021 00:44:32 +0100 Subject: [PATCH 045/496] Make MultiplexedPath.name a property Closes #216 --- importlib_resources/readers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index bb483a64..06c333c6 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -98,6 +98,7 @@ def joinpath(self, child): def open(self, *args, **kwargs): raise FileNotFoundError('{} is not a file'.format(self)) + @property def name(self): return self._paths[0].name From 6e2d0ba00b60c10466b0e040e2d4b1206c3f0b3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Apr 2021 20:38:58 -0400 Subject: [PATCH 046/496] 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 047/496] 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 048/496] 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 049/496] 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 ef37d6df1bc7974e3dd54b4ea46c497e2f34a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 12 May 2021 15:57:21 +0100 Subject: [PATCH 050/496] tests/files: add test for namespace packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/tests/test_files.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 36992d6f..3a61d297 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,6 +4,7 @@ import importlib_resources as resources from importlib_resources.abc import Traversable from . import data01 +from . import namespacedata01 from . import util @@ -35,5 +36,10 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): pass +class OpenNamespaceTests(FilesTests, unittest.TestCase): + def setUp(self): + self.data = namespacedata01 + + if __name__ == '__main__': unittest.main() From 556ffa57bc30aefaf404e7bffae393ef9853c013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 12 May 2021 15:57:54 +0100 Subject: [PATCH 051/496] tests/read: add test for namespace packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/tests/test_files.py | 3 ++- importlib_resources/tests/test_read.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 3a61d297..5de129b9 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,7 +4,6 @@ import importlib_resources as resources from importlib_resources.abc import Traversable from . import data01 -from . import namespacedata01 from . import util @@ -38,6 +37,8 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceTests(FilesTests, unittest.TestCase): def setUp(self): + from . import namespacedata01 + self.data = namespacedata01 diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 5422eea3..2616fbf5 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -56,5 +56,12 @@ def test_read_submodule_resource_by_name(self): self.assertEqual(result, b'\0\1\2\3') +class ReadNamespaceTests(ReadTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + if __name__ == '__main__': unittest.main() From fdd54356ceba126d5dfda0925ffa6195b729697f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 12 May 2021 16:24:33 +0100 Subject: [PATCH 052/496] tests: add test for contents() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/tests/test_contents.py | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 importlib_resources/tests/test_contents.py diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py new file mode 100644 index 00000000..83da82f4 --- /dev/null +++ b/importlib_resources/tests/test_contents.py @@ -0,0 +1,61 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util + + +class ContentsTests: + @property + def contents(self): + return sorted( + [el for el in list(resources.contents(self.data)) if el != '__pycache__'] + ) + + +class ContentsDiskTests(ContentsTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + def test_contents(self): + self.assertEqual( + self.contents, + [ + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + ], + ) + + +class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): + def test_contents(self): + self.assertEqual( + self.contents, + [ + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + ], + ) + + +class ContentsNamespaceTests(ContentsTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + def test_contents(self): + self.assertEqual( + self.contents, + [ + 'binary.file', + 'utf-16.file', + 'utf-8.file', + ], + ) From 3b810fb1cb4ee811172ee834cdba11bccb484de4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 May 2021 16:19:45 -0400 Subject: [PATCH 053/496] Correct typo in traversable --- importlib_resources/_py3.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index ff2a883c..788a26c7 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -152,9 +152,9 @@ def contents(package: Package) -> Iterable[str]: reader = _common.get_resource_reader(package) if reader is not None: return _ensure_sequence(reader.contents()) - transversable = _common.from_package(package) - if transversable.is_dir(): - return list(item.name for item in transversable.iterdir()) + traversable = _common.from_package(package) + if traversable.is_dir(): + return list(item.name for item in traversable.iterdir()) return [] From 427d61c3e47da93710f632cc85e8454667b4c9d2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 May 2021 16:57:31 -0400 Subject: [PATCH 054/496] Copy coverage omit definition from importlib_metadata. --- .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 0373285df9c5ee8975dd75b22de5066bf10bffc8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 May 2021 16:42:43 -0400 Subject: [PATCH 055/496] Implement MultiplexedPath.iterdir using 'unique_everseen'. --- importlib_resources/_itertools.py | 19 +++++++++++++++++++ importlib_resources/readers.py | 11 ++++------- 2 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 importlib_resources/_itertools.py diff --git a/importlib_resources/_itertools.py b/importlib_resources/_itertools.py new file mode 100644 index 00000000..dd45f2f0 --- /dev/null +++ b/importlib_resources/_itertools.py @@ -0,0 +1,19 @@ +from itertools import filterfalse + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 06c333c6..105267d3 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,8 +1,10 @@ import collections import pathlib +import operator from . import abc +from ._itertools import unique_everseen from ._compat import ZipPath @@ -65,13 +67,8 @@ def __init__(self, *paths): raise NotADirectoryError('MultiplexedPath only supports directories') def iterdir(self): - visited = [] - for path in self._paths: - for file in path.iterdir(): - if file.name in visited: - continue - visited.append(file.name) - yield file + files = (file for path in self._paths for file in path.iterdir()) + return unique_everseen(files, key=operator.attrgetter('name')) def read_bytes(self): raise FileNotFoundError(f'{self} is not a file') From 47938b7c671323ac8ec4f46e19f2b19004b88018 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 May 2021 16:56:25 -0400 Subject: [PATCH 056/496] Exclude _itertools from coverage. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..8e164f12 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + */_itertools.py [report] show_missing = True From d901af0d908053d11db556c7755dbce32e9d1311 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 May 2021 17:31:49 -0400 Subject: [PATCH 057/496] Consolidate some behavior and re-use 'set' comparison for less strict unordered comparisons. --- importlib_resources/tests/test_contents.py | 57 ++++++++-------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 83da82f4..2af70607 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -6,56 +6,37 @@ class ContentsTests: - @property - def contents(self): - return sorted( - [el for el in list(resources.contents(self.data)) if el != '__pycache__'] - ) + expected = { + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } + + def test_contents(self): + assert self.expected <= set(resources.contents(self.data)) class ContentsDiskTests(ContentsTests, unittest.TestCase): def setUp(self): self.data = data01 - def test_contents(self): - self.assertEqual( - self.contents, - [ - '__init__.py', - 'binary.file', - 'subdirectory', - 'utf-16.file', - 'utf-8.file', - ], - ) - class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): - def test_contents(self): - self.assertEqual( - self.contents, - [ - '__init__.py', - 'binary.file', - 'subdirectory', - 'utf-16.file', - 'utf-8.file', - ], - ) + pass class ContentsNamespaceTests(ContentsTests, unittest.TestCase): + expected = { + # no __init__ because of namespace design + # no subdirectory as incidental difference in fixture + 'binary.file', + 'utf-16.file', + 'utf-8.file', + } + def setUp(self): from . import namespacedata01 self.data = namespacedata01 - - def test_contents(self): - self.assertEqual( - self.contents, - [ - 'binary.file', - 'utf-16.file', - 'utf-8.file', - ], - ) From a024adf2c5235218aeb6d90817fe93b4e63496b9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 13 May 2021 09:38:19 -0400 Subject: [PATCH 058/496] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8eddf92d..5df757d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.1.3 +====== + +* Refresh packaging and improve tests. + v5.1.2 ====== From 0a888d8532c7941744494dbd43320476a40eda64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 20 May 2021 18:51:07 +0100 Subject: [PATCH 059/496] compat: fix zip traversables raise KeyError instead of FileNotFoundError on open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix[1] is available on zipp 3.1.0 and/or Python 3.10. [1] https://github.com/jaraco/zipp/commit/ffee5684095af4324bf11d7cba8b2a51b29d6d9c Signed-off-by: Filipe Laíns --- importlib_resources/_compat.py | 4 ++-- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index deb071b9..94d8ca43 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -5,9 +5,9 @@ import pathlib from contextlib import suppress -try: +if sys.version_info >= (3, 10): from zipfile import Path as ZipPath # type: ignore -except ImportError: +else: from zipp import Path as ZipPath # type: ignore diff --git a/setup.cfg b/setup.cfg index 4b32db80..976b3ddc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ packages = find_namespace: include_package_data = true python_requires = >=3.6 install_requires = - zipp >= 0.4; python_version < '3.8' + zipp >= 3.1.0; python_version < '3.10' setup_requires = setuptools_scm[toml] >= 3.4.1 [options.packages.find] From 4a734d4841b0ad5fddad3c2524e512f608c82d74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 May 2021 14:01:53 -0400 Subject: [PATCH 060/496] 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 c18e9678a97b78108bf3b58950371db26a50ea84 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 May 2021 12:20:41 -0400 Subject: [PATCH 061/496] Add changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5df757d6..79cdff3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.1.4 +====== + +* #225: Require + `zipp 3.1.0 `_ + or later on Python prior to 3.10 to incorporate those fixes. + v5.1.3 ====== From 1886e2b9d699c55afc1b75dc19e7026ad27e453a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 May 2021 13:53:26 -0400 Subject: [PATCH 062/496] Update docstrings to give more clarity on the intention of _compat.TraversableResourcesLoader and _compat.wrap_spec (seemingly duplicated). Fixes #227. --- importlib_resources/_compat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 94d8ca43..8740601b 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -29,6 +29,9 @@ class TraversableResourcesLoader: """ Adapt loaders to provide TraversableResources and other compatibility. + + Used primarily for Python 3.9 and earlier where the native + loaders do not yet implement TraversableResources. """ def __init__(self, spec): @@ -81,6 +84,9 @@ def wrap_spec(package): """ Construct a package spec with traversable compatibility on the spec/loader/reader. + + Supersedes _adapters.wrap_spec to use TraversableResourcesLoader + from above for older Python compatibility (<3.10). """ from . import _adapters From 156fc39803da067930d4d8fc9a3c9a78f42eeb88 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 May 2021 11:39:20 -0400 Subject: [PATCH 063/496] Add docstring to CommonTests.execute. No need for raise as it's already an abstract method. --- importlib_resources/tests/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 3247bcf1..89dc34f8 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -64,7 +64,10 @@ def create_package(file, path, is_package=True, contents=()): class CommonTests(metaclass=abc.ABCMeta): @abc.abstractmethod def execute(self, package, path): - raise NotImplementedError + """ + Call the pertinent legacy API function (e.g. open_text, path) + on package and path. + """ def test_package_name(self): # Passing in the package name should succeed. From 0b35342259c25c4b934adef3677bbfc5440898f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 May 2021 11:40:24 -0400 Subject: [PATCH 064/496] Refactor to remove unreachable branches and rely on 'any' for boolean evaluation. --- importlib_resources/tests/util.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 89dc34f8..c9f9eb93 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -25,25 +25,25 @@ def open_resource(self, path): self._path = path if isinstance(self.file, Exception): raise self.file - else: - return self.file + return self.file def resource_path(self, path_): self._path = path_ if isinstance(self.path, Exception): raise self.path - else: - return self.path + return self.path def is_resource(self, path_): self._path = path_ if isinstance(self.path, Exception): raise self.path - for entry in self._contents: - parts = entry.split('/') - if len(parts) == 1 and parts[0] == path_: - return True - return False + + def part(entry): + return entry.split('/') + + return any( + len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents) + ) def contents(self): if isinstance(self.path, Exception): From e1a0b908e3533f1b0d8d373d054fdf522b68a0bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 24 May 2021 12:27:01 -0400 Subject: [PATCH 065/496] Expand docstrings and faux-docstrings (boo unittest) to explain purpose of a couple of tests. Rename tests to reflect a more precise purpose. --- importlib_resources/tests/util.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index c9f9eb93..fbb208b5 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -62,6 +62,9 @@ def create_package(file, path, is_package=True, contents=()): class CommonTests(metaclass=abc.ABCMeta): + """ + Tests shared by test_open, test_path, and test_read. + """ @abc.abstractmethod def execute(self, package, path): """ @@ -115,14 +118,21 @@ def test_non_package_by_package(self): module = sys.modules['importlib_resources.tests.util'] self.execute(module, 'utf-8.file') - def test_resource_opener(self): + def test_missing_path(self): + # Attempting to open or read or request the path for a + # non-existent path should succeed if open_resource + # can return a viable data stream. bytes_data = io.BytesIO(b'Hello, world!') package = create_package(file=bytes_data, path=FileNotFoundError()) self.execute(package, 'utf-8.file') self.assertEqual(package.__loader__._path, 'utf-8.file') - def test_resource_path(self): + def test_extant_path(self): + # Attempting to open or read or request the path when the + # path does exist should still succeed. Does not assert + # anything about the result. bytes_data = io.BytesIO(b'Hello, world!') + # any path that exists path = __file__ package = create_package(file=bytes_data, path=path) self.execute(package, 'utf-8.file') From 1b165200642e74a4c2acebf7fedb28e732a17881 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 26 May 2021 10:40:46 -0400 Subject: [PATCH 066/496] 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 2319db42640cecab1e84c02b1d89f17c0d595da6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 26 May 2021 13:52:33 -0400 Subject: [PATCH 067/496] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_resources/tests/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index fbb208b5..206774b1 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -65,6 +65,7 @@ class CommonTests(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. """ + @abc.abstractmethod def execute(self, package, path): """ From bd4d6ffaa8d03159685503f5e728ad567932afbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Wed, 12 May 2021 17:07:11 +0100 Subject: [PATCH 068/496] use files() API in contents() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 2 +- importlib_resources/_common.py | 15 ++++++++++++++- importlib_resources/_py3.py | 21 ++------------------- importlib_resources/tests/test_resource.py | 10 +++++----- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index ea4c7efe..44e096a8 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -3,12 +3,12 @@ from ._common import ( as_file, files, + contents, ) from importlib_resources._py3 import ( Package, Resource, - contents, is_resource, open_binary, open_text, diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index d3223bdd..63f2642f 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Any, Optional +from typing import Union, Any, Optional, Iterable from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -113,3 +113,16 @@ def _(path): Degenerate behavior for pathlib.Path objects. """ yield path + + +# legacy API + + +def contents(package: Package) -> Iterable[str]: + """Return an iterable of entries in `package`. + + Note that not all entries are resources. Specifically, directories are + not considered resources. Use `is_resource()` on each entry returned here + to check if it is a resource or not. + """ + return [path.name for path in files(package).iterdir()] diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index 7f421259..d52ada3e 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -8,7 +8,7 @@ from io import BytesIO, TextIOWrapper from pathlib import Path from types import ModuleType -from typing import ContextManager, Iterable, Union +from typing import ContextManager, Union from typing import cast from typing.io import BinaryIO, TextIO from collections.abc import Sequence @@ -133,29 +133,12 @@ def is_resource(package: Package, name: str) -> bool: reader = _common.get_resource_reader(package) if reader is not None: return reader.is_resource(name) - package_contents = set(contents(package)) + package_contents = set(_common.contents(package)) if name not in package_contents: return False return (_common.from_package(package) / name).is_file() -def contents(package: Package) -> Iterable[str]: - """Return an iterable of entries in `package`. - - Note that not all entries are resources. Specifically, directories are - not considered resources. Use `is_resource()` on each entry returned here - to check if it is a resource or not. - """ - package = _common.get_package(package) - reader = _common.get_resource_reader(package) - if reader is not None: - return _ensure_sequence(reader.contents()) - traversable = _common.from_package(package) - if traversable.is_dir(): - return list(item.name for item in traversable.iterdir()) - return [] - - @singledispatch def _ensure_sequence(iterable): return list(iterable) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 75748e65..071cb8d4 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -33,14 +33,14 @@ def test_contents(self): # are not germane to this test, so just filter them out. contents.discard('__pycache__') self.assertEqual( - contents, - { + sorted(contents), + [ '__init__.py', - 'subdirectory', - 'utf-8.file', 'binary.file', + 'subdirectory', 'utf-16.file', - }, + 'utf-8.file', + ], ) From 4ff68cfc7ed2b4952c86887a43dc5ad0f8db1bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 13 May 2021 01:40:23 +0100 Subject: [PATCH 069/496] _adapters: replace DegenerateFiles with CompatibilityFiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/_adapters.py | 108 ++++++++++++++++++++++++++++--- importlib_resources/_compat.py | 3 +- 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/importlib_resources/_adapters.py b/importlib_resources/_adapters.py index eedde49d..9907b148 100644 --- a/importlib_resources/_adapters.py +++ b/importlib_resources/_adapters.py @@ -1,4 +1,5 @@ from contextlib import suppress +from io import TextIOWrapper from . import abc @@ -25,32 +26,119 @@ def __init__(self, spec): self.spec = spec def get_resource_reader(self, name): - return DegenerateFiles(self.spec)._native() + return CompatibilityFiles(self.spec)._native() -class DegenerateFiles: +def _io_wrapper(file, mode='r', *args, **kwargs): + if mode == 'r': + return TextIOWrapper(file, *args, **kwargs) + elif mode == 'rb': + return file + raise ValueError( + "Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode) + ) + + +class CompatibilityFiles: """ Adapter for an existing or non-existant resource reader - to provide a degenerate .files(). + to provide a compability .files(). """ - class Path(abc.Traversable): + class SpecPath(abc.Traversable): + """ + Path tied to a module spec. + Can be read and exposes the resource reader children. + """ + + def __init__(self, spec, reader): + self._spec = spec + self._reader = reader + + def iterdir(self): + if not self._reader: + return iter(()) + return iter( + CompatibilityFiles.ChildPath(self._reader, path) + for path in self._reader.contents() + ) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + if not self._reader: + return CompatibilityFiles.OrphanPath(other) + return CompatibilityFiles.ChildPath(self._reader, other) + + @property + def name(self): + return self._spec.name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs) + + class ChildPath(abc.Traversable): + """ + Path tied to a resource reader child. + Can be read but doesn't expose any meaningfull children. + """ + + def __init__(self, reader, name): + self._reader = reader + self._name = name + def iterdir(self): return iter(()) + def is_file(self): + return self._reader.is_resource(self.name) + def is_dir(self): + return not self.is_file() + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(self.name, other) + + @property + def name(self): + return self._name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper( + self._reader.open_resource(self.name), mode, *args, **kwargs + ) + + class OrphanPath(abc.Traversable): + """ + Orphan path, not tied to a module spec or resource reader. + Can't be read and doesn't expose any meaningful children. + """ + + def __init__(self, *path_parts): + if len(path_parts) < 1: + raise ValueError('Need at least one path part to construct a path') + self._path = path_parts + + def iterdir(self): + return iter(()) + + def is_file(self): return False - is_file = exists = is_dir # type: ignore + is_dir = is_file def joinpath(self, other): - return DegenerateFiles.Path() + return CompatibilityFiles.OrphanPath(*self._path, other) + @property def name(self): - return '' + return self._path[-1] - def open(self): - raise ValueError() + def open(self, mode='r', *args, **kwargs): + raise FileNotFoundError("Can't open orphan path") def __init__(self, spec): self.spec = spec @@ -71,7 +159,7 @@ def __getattr__(self, attr): return getattr(self._reader, attr) def files(self): - return DegenerateFiles.Path() + return CompatibilityFiles.SpecPath(self.spec, self._reader) def wrap_spec(package): diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 8740601b..0d1f72b7 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -76,7 +76,8 @@ def _file_reader(spec): or # local FileReader _file_reader(self.spec) - or _adapters.DegenerateFiles(self.spec) + # fallback - adapt the spec ResourceReader to TraversableReader + or _adapters.CompatibilityFiles(self.spec) ) From 53fc1c6793a019fec1b00843aa78e0cc32a1ee35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 20 May 2021 19:01:36 +0100 Subject: [PATCH 070/496] use files() api in open_* and read_* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 8 ++-- importlib_resources/_common.py | 39 ++++++++++++++++++ importlib_resources/_py3.py | 72 --------------------------------- 3 files changed, 43 insertions(+), 76 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 44e096a8..b8d609f7 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -4,17 +4,17 @@ as_file, files, contents, + open_binary, + read_binary, + open_text, + read_text, ) from importlib_resources._py3 import ( Package, Resource, is_resource, - open_binary, - open_text, path, - read_binary, - read_text, ) from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 63f2642f..838a9934 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -7,11 +7,13 @@ import importlib from typing import Union, Any, Optional, Iterable +from typing.io import BinaryIO, TextIO from .abc import ResourceReader, Traversable from ._compat import wrap_spec Package = Union[types.ModuleType, str] +Resource = Union[str, os.PathLike] def files(package): @@ -118,6 +120,43 @@ def _(path): # legacy API +def open_binary(package: Package, resource: Resource) -> BinaryIO: + """Return a file-like object opened for binary reading of the resource.""" + return (files(package) / normalize_path(resource)).open('rb') + + +def read_binary(package: Package, resource: Resource) -> bytes: + """Return the binary contents of the resource.""" + return (files(package) / normalize_path(resource)).read_bytes() + + +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: + """Return a file-like object opened for text reading of the resource.""" + return (files(package) / normalize_path(resource)).open( + 'r', encoding=encoding, errors=errors + ) + + +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: + """Return the decoded string of the resource. + + The decoding-related arguments have the same semantics as those of + bytes.decode(). + """ + with open_text(package, resource, encoding, errors) as fp: + return fp.read() + + def contents(package: Package) -> Iterable[str]: """Return an iterable of entries in `package`. diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index d52ada3e..43dce100 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -3,14 +3,9 @@ from . import _common from contextlib import suppress -from importlib.abc import ResourceLoader -from importlib.machinery import ModuleSpec -from io import BytesIO, TextIOWrapper from pathlib import Path from types import ModuleType from typing import ContextManager, Union -from typing import cast -from typing.io import BinaryIO, TextIO from collections.abc import Sequence from functools import singledispatch @@ -18,73 +13,6 @@ Resource = Union[str, os.PathLike] -def open_binary(package: Package, resource: Resource) -> BinaryIO: - """Return a file-like object opened for binary reading of the resource.""" - resource = _common.normalize_path(resource) - package = _common.get_package(package) - reader = _common.get_resource_reader(package) - if reader is not None: - return reader.open_resource(resource) - spec = cast(ModuleSpec, package.__spec__) - # Using pathlib doesn't work well here due to the lack of 'strict' - # argument for pathlib.Path.resolve() prior to Python 3.6. - if spec.submodule_search_locations is not None: - paths = spec.submodule_search_locations - elif spec.origin is not None: - paths = [os.path.dirname(os.path.abspath(spec.origin))] - - for package_path in paths: - full_path = os.path.join(package_path, resource) - try: - return open(full_path, mode='rb') - except OSError: - # Just assume the loader is a resource loader; all the relevant - # importlib.machinery loaders are and an AttributeError for - # get_data() will make it clear what is needed from the loader. - loader = cast(ResourceLoader, spec.loader) - data = None - if hasattr(spec.loader, 'get_data'): - with suppress(OSError): - data = loader.get_data(full_path) - if data is not None: - return BytesIO(data) - - raise FileNotFoundError(f'{resource!r} resource not found in {spec.name!r}') - - -def open_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> TextIO: - """Return a file-like object opened for text reading of the resource.""" - return TextIOWrapper( - open_binary(package, resource), encoding=encoding, errors=errors - ) - - -def read_binary(package: Package, resource: Resource) -> bytes: - """Return the binary contents of the resource.""" - with open_binary(package, resource) as fp: - return fp.read() - - -def read_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> str: - """Return the decoded string of the resource. - - The decoding-related arguments have the same semantics as those of - bytes.decode(). - """ - with open_text(package, resource, encoding, errors) as fp: - return fp.read() - - def path( package: Package, resource: Resource, From 30cf1a9feeb59fa1d38cde0d9f57e9d2812efeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Thu, 20 May 2021 19:39:51 +0100 Subject: [PATCH 071/496] use files() api in is_resource() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The files() api makes the identification of resources blury. Here we re-implement is_resource() by mirroring the previous logic to the best extent we can. This is not fully correct, as the files() api is not fully compatible with the legacy api, but it is pretty close and as correct as we can get. The semantics for what constitutes resources have always been blurry in the first place. Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 2 +- importlib_resources/_common.py | 12 ++++++++++++ importlib_resources/_py3.py | 16 ---------------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index b8d609f7..fcb31b39 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -8,12 +8,12 @@ read_binary, open_text, read_text, + is_resource, ) from importlib_resources._py3 import ( Package, Resource, - is_resource, path, ) from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 838a9934..d0b353b5 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -165,3 +165,15 @@ def contents(package: Package) -> Iterable[str]: to check if it is a resource or not. """ return [path.name for path in files(package).iterdir()] + + +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. + + Directories are *not* resources. + """ + resource = normalize_path(name) + return any( + traversable.name == resource and traversable.is_file() + for traversable in files(package).iterdir() + ) diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index 43dce100..3a791fc7 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -51,22 +51,6 @@ def _path_from_open_resource(reader, resource): return _common._tempfile(saved.read, suffix=resource) -def is_resource(package: Package, name: str) -> bool: - """True if `name` is a resource inside `package`. - - Directories are *not* resources. - """ - package = _common.get_package(package) - _common.normalize_path(name) - reader = _common.get_resource_reader(package) - if reader is not None: - return reader.is_resource(name) - package_contents = set(_common.contents(package)) - if name not in package_contents: - return False - return (_common.from_package(package) / name).is_file() - - @singledispatch def _ensure_sequence(iterable): return list(iterable) From b1c38ab32c72198adb3ee1f8a540cb7beb488a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 19:28:09 +0100 Subject: [PATCH 072/496] compat: fix selecting the correct reader if the spec is None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/_compat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 0d1f72b7..61e48d47 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -61,7 +61,11 @@ def _native_reader(spec): return reader if hasattr(reader, 'files') else None def _file_reader(spec): - if pathlib.Path(self.path).exists(): + try: + path = pathlib.Path(self.path) + except TypeError: + return None + if path.exists(): return readers.FileReader(self) return ( From 62e905c1cd3c37e73d9661fe67dff595bbbe9fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 19:28:42 +0100 Subject: [PATCH 073/496] use files() api in path() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 2 +- importlib_resources/_common.py | 17 +++++++++- importlib_resources/_py3.py | 56 +-------------------------------- 3 files changed, 18 insertions(+), 57 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index fcb31b39..1f710186 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -9,12 +9,12 @@ open_text, read_text, is_resource, + path, ) from importlib_resources._py3 import ( Package, Resource, - path, ) from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index d0b353b5..44650ca4 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Any, Optional, Iterable +from typing import Union, Any, Optional, Iterable, ContextManager from typing.io import BinaryIO, TextIO from .abc import ResourceReader, Traversable @@ -177,3 +177,18 @@ def is_resource(package: Package, name: str) -> bool: traversable.name == resource and traversable.is_file() for traversable in files(package).iterdir() ) + + +def path( + package: Package, + resource: Resource, +) -> ContextManager[pathlib.Path]: + """A context manager providing a file path object to the resource. + + If the resource does not already exist on its own on the file system, + a temporary file will be created. If the file was created, the file + will be deleted upon exiting the context manager (no exception is + raised if the file was deleted prior to the context manager + exiting). + """ + return as_file(files(package) / normalize_path(resource)) diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index 3a791fc7..b7f541b7 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -1,61 +1,7 @@ import os -import io -from . import _common -from contextlib import suppress -from pathlib import Path from types import ModuleType -from typing import ContextManager, Union -from collections.abc import Sequence -from functools import singledispatch +from typing import Union Package = Union[str, ModuleType] Resource = Union[str, os.PathLike] - - -def path( - package: Package, - resource: Resource, -) -> 'ContextManager[Path]': - """A context manager providing a file path object to the resource. - - If the resource does not already exist on its own on the file system, - a temporary file will be created. If the file was created, the file - will be deleted upon exiting the context manager (no exception is - raised if the file was deleted prior to the context manager - exiting). - """ - reader = _common.get_resource_reader(_common.get_package(package)) - return ( - _path_from_reader(reader, _common.normalize_path(resource)) - if reader - else _common.as_file( - _common.files(package).joinpath(_common.normalize_path(resource)) - ) - ) - - -def _path_from_reader(reader, resource): - return _path_from_resource_path(reader, resource) or _path_from_open_resource( - reader, resource - ) - - -def _path_from_resource_path(reader, resource): - with suppress(FileNotFoundError): - return Path(reader.resource_path(resource)) - - -def _path_from_open_resource(reader, resource): - saved = io.BytesIO(reader.open_resource(resource).read()) - return _common._tempfile(saved.read, suffix=resource) - - -@singledispatch -def _ensure_sequence(iterable): - return list(iterable) - - -@_ensure_sequence.register(Sequence) -def _(iterable): - return iterable From c2d85c9ac08a186c88aa7a441c9c5c032034d2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 19:30:46 +0100 Subject: [PATCH 074/496] remove _py3 implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 4 +--- importlib_resources/_py3.py | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 importlib_resources/_py3.py diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 1f710186..d4136aa2 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -10,12 +10,10 @@ read_text, is_resource, path, -) - -from importlib_resources._py3 import ( Package, Resource, ) + from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py deleted file mode 100644 index b7f541b7..00000000 --- a/importlib_resources/_py3.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -from types import ModuleType -from typing import Union - -Package = Union[str, ModuleType] -Resource = Union[str, os.PathLike] From 5e8c1d2d0b5b8550623b2410b9f2a26b0e46f1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 19:34:33 +0100 Subject: [PATCH 075/496] common: ignore PermissionError in _tempfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 44650ca4..10cafc41 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -95,7 +95,7 @@ def _tempfile(reader, suffix=''): finally: try: os.remove(raw_path) - except FileNotFoundError: + except (FileNotFoundError, PermissionError): pass From 97083d2b3eb6a5e9fc91943bf61eeb23fa7134a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 20:27:05 +0100 Subject: [PATCH 076/496] tests: add tests for CompatibiltyFiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- .../tests/test_compatibilty_files.py | 95 +++++++++++++++++++ importlib_resources/tests/util.py | 10 +- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 importlib_resources/tests/test_compatibilty_files.py diff --git a/importlib_resources/tests/test_compatibilty_files.py b/importlib_resources/tests/test_compatibilty_files.py new file mode 100644 index 00000000..c6ece7e6 --- /dev/null +++ b/importlib_resources/tests/test_compatibilty_files.py @@ -0,0 +1,95 @@ +import io +import unittest + +import importlib_resources as resources + +from importlib_resources._adapters import CompatibilityFiles + +from . import util + + +class CompatibilityFilesTests(unittest.TestCase): + @property + def package(self): + bytes_data = io.BytesIO(b'Hello, world!') + return util.create_package( + file=bytes_data, + path='some_path', + contents=('a', 'b', 'c'), + ) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_iter(self): + self.assertEqual( + sorted(path.name for path in self.files.iterdir()), + ['a', 'b', 'c'], + ) + + def test_child_path_iter(self): + self.assertEqual(list((self.files / 'a').iterdir()), []) + + def test_orphan_path_iter(self): + self.assertEqual(list((self.files / 'a' / 'a').iterdir()), []) + self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), []) + + def test_spec_path_is(self): + self.assertFalse(self.files.is_file()) + self.assertFalse(self.files.is_dir()) + + def test_child_path_is(self): + self.assertTrue((self.files / 'a').is_file()) + self.assertFalse((self.files / 'a').is_dir()) + + def test_orphan_path_is(self): + self.assertFalse((self.files / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a').is_dir()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir()) + + def test_spec_path_name(self): + self.assertEqual(self.files.name, 'testingpackage') + + def test_child_path_name(self): + self.assertEqual((self.files / 'a').name, 'a') + + def test_orphan_path_name(self): + self.assertEqual((self.files / 'a' / 'b').name, 'b') + self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c') + + def test_spec_path_open(self): + self.assertEqual(self.files.read_bytes(), b'Hello, world!') + self.assertEqual(self.files.read_text(), 'Hello, world!') + + def test_child_path_open(self): + self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') + self.assertEqual((self.files / 'a').read_text(), 'Hello, world!') + + def test_orphan_path_open(self): + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b').read_bytes() + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b' / 'c').read_bytes() + + def test_open_invalid_mode(self): + with self.assertRaises(ValueError): + self.files.open('0') + + def test_orphan_path_invalid(self): + with self.assertRaises(ValueError): + CompatibilityFiles.OrphanPath() + + +class CompatibilityFilesNoReaderTests(unittest.TestCase): + @property + def package(self): + return util.create_package_from_loader(None) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_joinpath(self): + self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 206774b1..6ac4332e 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -51,16 +51,22 @@ def contents(self): yield from self._contents -def create_package(file, path, is_package=True, contents=()): +def create_package_from_loader(loader, is_package=True): name = 'testingpackage' module = types.ModuleType(name) - loader = Reader(file=file, path=path, _contents=contents) spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package) module.__spec__ = spec module.__loader__ = loader return module +def create_package(file=None, path=None, is_package=True, contents=()): + return create_package_from_loader( + Reader(file=file, path=path, _contents=contents), + is_package, + ) + + class CommonTests(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. From ef2f7c2f263c3b52d896eab213609f2a94879907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 29 May 2021 20:32:32 +0100 Subject: [PATCH 077/496] tests: add test for CompatibilityFiles wrap_spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/tests/test_compatibilty_files.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/importlib_resources/tests/test_compatibilty_files.py b/importlib_resources/tests/test_compatibilty_files.py index c6ece7e6..d92c7c56 100644 --- a/importlib_resources/tests/test_compatibilty_files.py +++ b/importlib_resources/tests/test_compatibilty_files.py @@ -3,7 +3,10 @@ import importlib_resources as resources -from importlib_resources._adapters import CompatibilityFiles +from importlib_resources._adapters import ( + CompatibilityFiles, + wrap_spec, +) from . import util @@ -81,6 +84,10 @@ def test_orphan_path_invalid(self): with self.assertRaises(ValueError): CompatibilityFiles.OrphanPath() + def test_wrap_spec(self): + spec = wrap_spec(self.package) + self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles) + class CompatibilityFilesNoReaderTests(unittest.TestCase): @property From 85d08db3ef3811bd208995254e7e9c9658cf710d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 3 Jun 2021 19:55:51 -0400 Subject: [PATCH 078/496] 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 079/496] 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 e016e668de95f6c4fa76741b5631627cd26d2aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Sat, 26 Jun 2021 20:41:52 +0100 Subject: [PATCH 080/496] legacy: move legacy API to separate module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/__init__.py | 7 ++- importlib_resources/_common.py | 80 +------------------------------ importlib_resources/_legacy.py | 85 +++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 81 deletions(-) create mode 100644 importlib_resources/_legacy.py diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index d4136aa2..2468f575 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -3,6 +3,11 @@ from ._common import ( as_file, files, + Package, + Resource, +) + +from ._legacy import ( contents, open_binary, read_binary, @@ -10,8 +15,6 @@ read_text, is_resource, path, - Package, - Resource, ) from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 10cafc41..9bb6bda5 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,8 +6,7 @@ import types import importlib -from typing import Union, Any, Optional, Iterable, ContextManager -from typing.io import BinaryIO, TextIO +from typing import Union, Any, Optional from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -115,80 +114,3 @@ def _(path): Degenerate behavior for pathlib.Path objects. """ yield path - - -# legacy API - - -def open_binary(package: Package, resource: Resource) -> BinaryIO: - """Return a file-like object opened for binary reading of the resource.""" - return (files(package) / normalize_path(resource)).open('rb') - - -def read_binary(package: Package, resource: Resource) -> bytes: - """Return the binary contents of the resource.""" - return (files(package) / normalize_path(resource)).read_bytes() - - -def open_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> TextIO: - """Return a file-like object opened for text reading of the resource.""" - return (files(package) / normalize_path(resource)).open( - 'r', encoding=encoding, errors=errors - ) - - -def read_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> str: - """Return the decoded string of the resource. - - The decoding-related arguments have the same semantics as those of - bytes.decode(). - """ - with open_text(package, resource, encoding, errors) as fp: - return fp.read() - - -def contents(package: Package) -> Iterable[str]: - """Return an iterable of entries in `package`. - - Note that not all entries are resources. Specifically, directories are - not considered resources. Use `is_resource()` on each entry returned here - to check if it is a resource or not. - """ - return [path.name for path in files(package).iterdir()] - - -def is_resource(package: Package, name: str) -> bool: - """True if `name` is a resource inside `package`. - - Directories are *not* resources. - """ - resource = normalize_path(name) - return any( - traversable.name == resource and traversable.is_file() - for traversable in files(package).iterdir() - ) - - -def path( - package: Package, - resource: Resource, -) -> ContextManager[pathlib.Path]: - """A context manager providing a file path object to the resource. - - If the resource does not already exist on its own on the file system, - a temporary file will be created. If the file was created, the file - will be deleted upon exiting the context manager (no exception is - raised if the file was deleted prior to the context manager - exiting). - """ - return as_file(files(package) / normalize_path(resource)) diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py new file mode 100644 index 00000000..3b9a5342 --- /dev/null +++ b/importlib_resources/_legacy.py @@ -0,0 +1,85 @@ +import os +import pathlib +import types + +from typing import Union, Iterable, ContextManager +from typing.io import BinaryIO, TextIO + +from . import _common + +Package = Union[types.ModuleType, str] +Resource = Union[str, os.PathLike] + + +def open_binary(package: Package, resource: Resource) -> BinaryIO: + """Return a file-like object opened for binary reading of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).open('rb') + + +def read_binary(package: Package, resource: Resource) -> bytes: + """Return the binary contents of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).read_bytes() + + +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: + """Return a file-like object opened for text reading of the resource.""" + return (_common.files(package) / _common.normalize_path(resource)).open( + 'r', encoding=encoding, errors=errors + ) + + +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: + """Return the decoded string of the resource. + + The decoding-related arguments have the same semantics as those of + bytes.decode(). + """ + with open_text(package, resource, encoding, errors) as fp: + return fp.read() + + +def contents(package: Package) -> Iterable[str]: + """Return an iterable of entries in `package`. + + Note that not all entries are resources. Specifically, directories are + not considered resources. Use `is_resource()` on each entry returned here + to check if it is a resource or not. + """ + return [path.name for path in _common.files(package).iterdir()] + + +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. + + Directories are *not* resources. + """ + resource = _common.normalize_path(name) + return any( + traversable.name == resource and traversable.is_file() + for traversable in _common.files(package).iterdir() + ) + + +def path( + package: Package, + resource: Resource, +) -> ContextManager[pathlib.Path]: + """A context manager providing a file path object to the resource. + + If the resource does not already exist on its own on the file system, + a temporary file will be created. If the file was created, the file + will be deleted upon exiting the context manager (no exception is + raised if the file was deleted prior to the context manager + exiting). + """ + return _common.as_file(_common.files(package) / _common.normalize_path(resource)) From fc2bce91239cb0067e4c4f53ecdd5a48a86417af Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Jun 2021 20:13:12 -0400 Subject: [PATCH 081/496] Update changelog. Ref #221. --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 659ea2a1..3835bcfa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +v5.2.0 +====== + +* #80 via #221: Legacy API (``path``, ``contents``, ...) + is now supported entirely by the ``.files()`` API with + a compatibility shim supplied for resource loaders without + that functionality. + v5.0.6 ====== From 14787e69e793d68c8ac17f010dc45891ee0a492c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jul 2021 09:03:21 -0400 Subject: [PATCH 082/496] 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 083/496] 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 084/496] 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 085/496] 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 086/496] 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 e7c4baec71d13fb4b05e4910ca2289b4e289cd3a Mon Sep 17 00:00:00 2001 From: wwuck <301402+wwuck@users.noreply.github.com> Date: Mon, 26 Jul 2021 22:24:36 +1000 Subject: [PATCH 087/496] docs/migration: explicitly set binary mode for open() (#231) open() uses non-binary by default. The equivalent pkg_resources.resource_stream() method opens in binary mode. --- docs/migration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration.rst b/docs/migration.rst index 04d059cc..6828f0b8 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -91,7 +91,7 @@ bytes. E.g.:: The equivalent code in ``importlib_resources`` is pretty straightforward:: ref = importlib_resources.files('my.package').joinpath('resource.dat') - with ref.open() as fp: + with ref.open('rb') as fp: my_bytes = fp.read() From 87c36efdf27c94ebd3826cd5ac8cf7ece9ffb97b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Jul 2021 21:52:04 -0400 Subject: [PATCH 088/496] bpo-38291: DeprecationWarning when importing typing.{io,re} --- importlib_resources/_legacy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 3b9a5342..2ddec5f9 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -2,8 +2,7 @@ import pathlib import types -from typing import Union, Iterable, ContextManager -from typing.io import BinaryIO, TextIO +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO from . import _common From 66ea2dc7eb12b1be2322b7ad002cefb12d364dff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Jul 2021 21:52:04 -0400 Subject: [PATCH 089/496] bpo-38291: DeprecationWarning when importing typing.{io,re} --- CHANGES.rst | 5 +++++ importlib_resources/_legacy.py | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3835bcfa..c2a6e9cc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.2.1 +====== + +* bpo-38291: Avoid DeprecationWarning on ``typing.io``. + v5.2.0 ====== diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 3b9a5342..2ddec5f9 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -2,8 +2,7 @@ import pathlib import types -from typing import Union, Iterable, ContextManager -from typing.io import BinaryIO, TextIO +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO from . import _common From b5fd3461a900abd51c4912ca86e81a31318286c2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Jul 2021 16:49:13 -0400 Subject: [PATCH 090/496] Avoid file descriptor refleaks in as_file. --- importlib_resources/_common.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 9bb6bda5..25511672 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -87,14 +87,16 @@ def _tempfile(reader, suffix=''): # properly. fd, raw_path = tempfile.mkstemp(suffix=suffix) try: - os.write(fd, reader()) - os.close(fd) + try: + os.write(fd, reader()) + finally: + os.close(fd) del reader yield pathlib.Path(raw_path) finally: try: os.remove(raw_path) - except (FileNotFoundError, PermissionError): + except FileNotFoundError: pass From 778172d18cdb9a971b7d0418f0d668045588fd60 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Jul 2021 16:55:07 -0400 Subject: [PATCH 091/496] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c2a6e9cc..71f8f403 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.2.2 +====== + +* #234: Fix refleak in ``as_file`` caught by CPython tests. + v5.2.1 ====== 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 092/496] .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 bcb63aeae2c111ce528756b61d0b7d6cdfb1b2bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Sep 2021 20:33:17 -0400 Subject: [PATCH 093/496] Include type annotations from more_itertools, plus define type for 'seen'. --- importlib_resources/_itertools.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_itertools.py b/importlib_resources/_itertools.py index dd45f2f0..cce05582 100644 --- a/importlib_resources/_itertools.py +++ b/importlib_resources/_itertools.py @@ -1,11 +1,27 @@ from itertools import filterfalse +from typing import ( + Callable, + Iterable, + Iterator, + Optional, + Set, + TypeVar, + Union, +) -def unique_everseen(iterable, key=None): +# Type and type variable definitions +_T = TypeVar('_T') +_U = TypeVar('_U') + + +def unique_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None +) -> Iterator[_T]: "List unique elements, preserving order. Remember all elements ever seen." # unique_everseen('AAAABBBCCDAABBB') --> A B C D # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen = set() + seen: Set[Union[_T, _U]] = set() seen_add = seen.add if key is None: for element in filterfalse(seen.__contains__, iterable): From 8ea55f2fb26bd77997f0e9435bab2d41376a76d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Sep 2021 21:23:38 -0400 Subject: [PATCH 094/496] 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 095/496] 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 096/496] 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 767bc872d6bcefc2923d52ac87eae6d4fe114bb1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 15:05:16 -0400 Subject: [PATCH 097/496] Remove section of the readme that indicates deferring to stdlib. This project hasn't implemented that behavior for quite some time. --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index c9c692ac..2215e0f1 100644 --- a/README.rst +++ b/README.rst @@ -23,9 +23,7 @@ ``importlib_resources`` is a backport of Python standard library `importlib.resources `_ -module for older Pythons. Users of Python 3.9 and beyond -should use the standard library module, since for these versions, -``importlib_resources`` just delegates to that module. +module for older Pythons. The key goal of this module is to replace parts of `pkg_resources `_ with a From 5a735feffae8ba6ffb9670f92013f905b752e771 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 15:31:47 -0400 Subject: [PATCH 098/496] Add compatibility matrix showing relevant versions. --- README.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.rst b/README.rst index 2215e0f1..29aef77d 100644 --- a/README.rst +++ b/README.rst @@ -30,3 +30,24 @@ The key goal of this module is to replace parts of `pkg_resources solution in Python's stdlib that relies on well-defined APIs. This makes reading resources included in packages easier, with more stable and consistent semantics. + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - importlib_resources + - stdlib + * - 5.2 + - 3.11 + * - 5.0 + - 3.10 + * - 1.3 + - 3.9 + * - 0.5 (?) + - 3.7 From b665a3ea907d93b1b6457217f34e1bfc06f51fe6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 15:32:38 -0400 Subject: [PATCH 099/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 10eb17ef..859dbf18 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.2.3 +====== + +* Updated readme to reflect current behavior and show + which versions correspond to which behavior in CPython. + v5.0.7 ====== From 0ad6143b5c50b55396947bb6a9632e7578b93f8d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 17:41:17 -0400 Subject: [PATCH 100/496] Emit deprecation warning on legacy functions. --- importlib_resources/_legacy.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 2ddec5f9..a5d49e53 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -1,6 +1,8 @@ +import inspect import os import pathlib import types +import warnings from typing import Union, Iterable, ContextManager, BinaryIO, TextIO @@ -10,13 +12,24 @@ Resource = Union[str, os.PathLike] +def _warn(): + func = inspect.stack()[1].function + warnings.warn( + f"{func} is deprecated. Use files() instead.", + DeprecationWarning, + stacklevel=2, + ) + + def open_binary(package: Package, resource: Resource) -> BinaryIO: """Return a file-like object opened for binary reading of the resource.""" + _warn() return (_common.files(package) / _common.normalize_path(resource)).open('rb') def read_binary(package: Package, resource: Resource) -> bytes: """Return the binary contents of the resource.""" + _warn() return (_common.files(package) / _common.normalize_path(resource)).read_bytes() @@ -27,6 +40,7 @@ def open_text( errors: str = 'strict', ) -> TextIO: """Return a file-like object opened for text reading of the resource.""" + _warn() return (_common.files(package) / _common.normalize_path(resource)).open( 'r', encoding=encoding, errors=errors ) @@ -43,6 +57,7 @@ def read_text( The decoding-related arguments have the same semantics as those of bytes.decode(). """ + _warn() with open_text(package, resource, encoding, errors) as fp: return fp.read() @@ -54,6 +69,7 @@ def contents(package: Package) -> Iterable[str]: not considered resources. Use `is_resource()` on each entry returned here to check if it is a resource or not. """ + _warn() return [path.name for path in _common.files(package).iterdir()] @@ -62,6 +78,7 @@ def is_resource(package: Package, name: str) -> bool: Directories are *not* resources. """ + _warn() resource = _common.normalize_path(name) return any( traversable.name == resource and traversable.is_file() @@ -81,4 +98,5 @@ def path( raised if the file was deleted prior to the context manager exiting). """ + _warn() return _common.as_file(_common.files(package) / _common.normalize_path(resource)) From 6065e26c049c8323676bee166bd63f101c4f44c2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 17:45:40 -0400 Subject: [PATCH 101/496] Re-implement deprecation as a decorator. --- importlib_resources/_legacy.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index a5d49e53..7526d512 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -1,4 +1,4 @@ -import inspect +import functools import os import pathlib import types @@ -12,27 +12,32 @@ Resource = Union[str, os.PathLike] -def _warn(): - func = inspect.stack()[1].function - warnings.warn( - f"{func} is deprecated. Use files() instead.", - DeprecationWarning, - stacklevel=2, - ) +def deprecated(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated. Use files() instead.", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper +@deprecated def open_binary(package: Package, resource: Resource) -> BinaryIO: """Return a file-like object opened for binary reading of the resource.""" - _warn() return (_common.files(package) / _common.normalize_path(resource)).open('rb') +@deprecated def read_binary(package: Package, resource: Resource) -> bytes: """Return the binary contents of the resource.""" - _warn() return (_common.files(package) / _common.normalize_path(resource)).read_bytes() +@deprecated def open_text( package: Package, resource: Resource, @@ -40,12 +45,12 @@ def open_text( errors: str = 'strict', ) -> TextIO: """Return a file-like object opened for text reading of the resource.""" - _warn() return (_common.files(package) / _common.normalize_path(resource)).open( 'r', encoding=encoding, errors=errors ) +@deprecated def read_text( package: Package, resource: Resource, @@ -57,11 +62,11 @@ def read_text( The decoding-related arguments have the same semantics as those of bytes.decode(). """ - _warn() with open_text(package, resource, encoding, errors) as fp: return fp.read() +@deprecated def contents(package: Package) -> Iterable[str]: """Return an iterable of entries in `package`. @@ -69,16 +74,15 @@ def contents(package: Package) -> Iterable[str]: not considered resources. Use `is_resource()` on each entry returned here to check if it is a resource or not. """ - _warn() return [path.name for path in _common.files(package).iterdir()] +@deprecated def is_resource(package: Package, name: str) -> bool: """True if `name` is a resource inside `package`. Directories are *not* resources. """ - _warn() resource = _common.normalize_path(name) return any( traversable.name == resource and traversable.is_file() @@ -86,6 +90,7 @@ def is_resource(package: Package, name: str) -> bool: ) +@deprecated def path( package: Package, resource: Resource, @@ -98,5 +103,4 @@ def path( raised if the file was deleted prior to the context manager exiting). """ - _warn() return _common.as_file(_common.files(package) / _common.normalize_path(resource)) From 13d42b7ad1376649a45fa86f7baa35dc289aa9e3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 17:51:08 -0400 Subject: [PATCH 102/496] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 859dbf18..bfe4f49f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.3.0 +====== + +* #80: Now raise a ``DeprecationWarning`` for all legacy + functions. Instead, users should rely on the ``files()`` + API introduced in importlib_resources 1.3. + v5.2.3 ====== From fda6cb2423e839d0920c2bdafe977eef65309656 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 18:00:54 -0400 Subject: [PATCH 103/496] Acknowledge and suppress warnings --- importlib_resources/tests/test_contents.py | 3 +- importlib_resources/tests/test_open.py | 51 ++++++---- importlib_resources/tests/test_path.py | 28 +++--- importlib_resources/tests/test_read.py | 29 ++++-- importlib_resources/tests/test_resource.py | 103 +++++++++++++-------- importlib_resources/tests/util.py | 9 ++ 6 files changed, 143 insertions(+), 80 deletions(-) diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 2af70607..c95e1a77 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -15,7 +15,8 @@ class ContentsTests: } def test_contents(self): - assert self.expected <= set(resources.contents(self.data)) + with util.suppress_known_deprecation(): + assert self.expected <= set(resources.contents(self.data)) class ContentsDiskTests(ContentsTests, unittest.TestCase): diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 919fc0db..1c8a943a 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -7,38 +7,47 @@ class CommonBinaryTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with resources.open_binary(package, path): - pass + with util.suppress_known_deprecation(): + with resources.open_binary(package, path): + pass class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with resources.open_text(package, path): - pass + with util.suppress_known_deprecation(): + with resources.open_text(package, path): + pass class OpenTests: def test_open_binary(self): - with resources.open_binary(self.data, 'utf-8.file') as fp: - result = fp.read() + with util.suppress_known_deprecation(): + with resources.open_binary(self.data, 'utf-8.file') as fp: + result = fp.read() self.assertEqual(result, b'Hello, UTF-8 world!\n') def test_open_text_default_encoding(self): - with resources.open_text(self.data, 'utf-8.file') as fp: - result = fp.read() + with util.suppress_known_deprecation(): + with resources.open_text(self.data, 'utf-8.file') as fp: + result = fp.read() self.assertEqual(result, 'Hello, UTF-8 world!\n') def test_open_text_given_encoding(self): - with resources.open_text(self.data, 'utf-16.file', 'utf-16', 'strict') as fp: - result = fp.read() + with util.suppress_known_deprecation(): + with resources.open_text( + self.data, 'utf-16.file', 'utf-16', 'strict' + ) as fp: + result = fp.read() self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_open_text_with_errors(self): # Raises UnicodeError without the 'errors' argument. - with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'strict') as fp: - self.assertRaises(UnicodeError, fp.read) - with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'ignore') as fp: - result = fp.read() + with util.suppress_known_deprecation(): + with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'strict') as fp: + self.assertRaises(UnicodeError, fp.read) + with util.suppress_known_deprecation(): + with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'ignore') as fp: + result = fp.read() self.assertEqual( result, 'H\x00e\x00l\x00l\x00o\x00,\x00 ' @@ -47,14 +56,16 @@ def test_open_text_with_errors(self): ) def test_open_binary_FileNotFoundError(self): - self.assertRaises( - FileNotFoundError, resources.open_binary, self.data, 'does-not-exist' - ) + with util.suppress_known_deprecation(): + self.assertRaises( + FileNotFoundError, resources.open_binary, self.data, 'does-not-exist' + ) def test_open_text_FileNotFoundError(self): - self.assertRaises( - FileNotFoundError, resources.open_text, self.data, 'does-not-exist' - ) + with util.suppress_known_deprecation(): + self.assertRaises( + FileNotFoundError, resources.open_text, self.data, 'does-not-exist' + ) class OpenDiskTests(OpenTests, unittest.TestCase): diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index 9180626f..af433ce0 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -8,8 +8,9 @@ class CommonTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with resources.path(package, path): - pass + with util.suppress_known_deprecation(): + with resources.path(package, path): + pass class PathTests: @@ -17,12 +18,13 @@ def test_reading(self): # Path should be readable. # Test also implicitly verifies the returned object is a pathlib.Path # instance. - with resources.path(self.data, 'utf-8.file') as path: - self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) - # pathlib.Path.read_text() was introduced in Python 3.5. - with path.open('r', encoding='utf-8') as file: - text = file.read() - self.assertEqual('Hello, UTF-8 world!\n', text) + with util.suppress_known_deprecation(): + with resources.path(self.data, 'utf-8.file') as path: + self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) + # pathlib.Path.read_text() was introduced in Python 3.5. + with path.open('r', encoding='utf-8') as file: + text = file.read() + self.assertEqual('Hello, UTF-8 world!\n', text) class PathDiskTests(PathTests, unittest.TestCase): @@ -34,8 +36,9 @@ def test_natural_path(self): file-system-backed resources do not get the tempdir treatment. """ - with resources.path(self.data, 'utf-8.file') as path: - assert 'data' in str(path) + with util.suppress_known_deprecation(): + with resources.path(self.data, 'utf-8.file') as path: + assert 'data' in str(path) class PathMemoryTests(PathTests, unittest.TestCase): @@ -53,8 +56,9 @@ class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): def test_remove_in_context_manager(self): # It is not an error if the file that was temporarily stashed on the # file system is removed inside the `with` stanza. - with resources.path(self.data, 'utf-8.file') as path: - path.unlink() + with util.suppress_known_deprecation(): + with resources.path(self.data, 'utf-8.file') as path: + path.unlink() if __name__ == '__main__': diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 2616fbf5..da7d2745 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -8,31 +8,40 @@ class CommonBinaryTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - resources.read_binary(package, path) + with util.suppress_known_deprecation(): + resources.read_binary(package, path) class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - resources.read_text(package, path) + with util.suppress_known_deprecation(): + resources.read_text(package, path) class ReadTests: def test_read_binary(self): - result = resources.read_binary(self.data, 'binary.file') + with util.suppress_known_deprecation(): + result = resources.read_binary(self.data, 'binary.file') self.assertEqual(result, b'\0\1\2\3') def test_read_text_default_encoding(self): - result = resources.read_text(self.data, 'utf-8.file') + with util.suppress_known_deprecation(): + result = resources.read_text(self.data, 'utf-8.file') self.assertEqual(result, 'Hello, UTF-8 world!\n') def test_read_text_given_encoding(self): - result = resources.read_text(self.data, 'utf-16.file', encoding='utf-16') + with util.suppress_known_deprecation(): + result = resources.read_text(self.data, 'utf-16.file', encoding='utf-16') self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_read_text_with_errors(self): # Raises UnicodeError without the 'errors' argument. - self.assertRaises(UnicodeError, resources.read_text, self.data, 'utf-16.file') - result = resources.read_text(self.data, 'utf-16.file', errors='ignore') + with util.suppress_known_deprecation(): + self.assertRaises( + UnicodeError, resources.read_text, self.data, 'utf-16.file' + ) + with util.suppress_known_deprecation(): + result = resources.read_text(self.data, 'utf-16.file', errors='ignore') self.assertEqual( result, 'H\x00e\x00l\x00l\x00o\x00,\x00 ' @@ -48,11 +57,13 @@ class ReadDiskTests(ReadTests, unittest.TestCase): class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('ziptestdata.subdirectory') - result = resources.read_binary(submodule, 'binary.file') + with util.suppress_known_deprecation(): + result = resources.read_binary(submodule, 'binary.file') self.assertEqual(result, b'\0\1\2\3') def test_read_submodule_resource_by_name(self): - result = resources.read_binary('ziptestdata.subdirectory', 'binary.file') + with util.suppress_known_deprecation(): + result = resources.read_binary('ziptestdata.subdirectory', 'binary.file') self.assertEqual(result, b'\0\1\2\3') diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 071cb8d4..9fcf6705 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -15,17 +15,21 @@ class ResourceTests: # Subclasses are expected to set the `data` attribute. def test_is_resource_good_path(self): - self.assertTrue(resources.is_resource(self.data, 'binary.file')) + with util.suppress_known_deprecation(): + self.assertTrue(resources.is_resource(self.data, 'binary.file')) def test_is_resource_missing(self): - self.assertFalse(resources.is_resource(self.data, 'not-a-file')) + with util.suppress_known_deprecation(): + self.assertFalse(resources.is_resource(self.data, 'not-a-file')) def test_is_resource_subresource_directory(self): # Directories are not resources. - self.assertFalse(resources.is_resource(self.data, 'subdirectory')) + with util.suppress_known_deprecation(): + self.assertFalse(resources.is_resource(self.data, 'subdirectory')) def test_contents(self): - contents = set(resources.contents(self.data)) + with util.suppress_known_deprecation(): + contents = set(resources.contents(self.data)) # There may be cruft in the directory listing of the data directory. # It could have a __pycache__ directory, # an artifact of the @@ -58,25 +62,29 @@ def test_resource_contents(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C'] ) - self.assertEqual(set(resources.contents(package)), {'A', 'B', 'C'}) + with util.suppress_known_deprecation(): + self.assertEqual(set(resources.contents(package)), {'A', 'B', 'C'}) def test_resource_is_resource(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - self.assertTrue(resources.is_resource(package, 'B')) + with util.suppress_known_deprecation(): + self.assertTrue(resources.is_resource(package, 'B')) def test_resource_directory_is_not_resource(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - self.assertFalse(resources.is_resource(package, 'D')) + with util.suppress_known_deprecation(): + self.assertFalse(resources.is_resource(package, 'D')) def test_resource_missing_is_not_resource(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - self.assertFalse(resources.is_resource(package, 'Z')) + with util.suppress_known_deprecation(): + self.assertFalse(resources.is_resource(package, 'Z')) class ResourceCornerCaseTests(unittest.TestCase): @@ -94,7 +102,8 @@ def test_package_has_no_reader_fallback(self): module.__file__ = '/path/which/shall/not/be/named' module.__spec__.loader = module.__loader__ module.__spec__.origin = module.__file__ - self.assertFalse(resources.is_resource(module, 'A')) + with util.suppress_known_deprecation(): + self.assertFalse(resources.is_resource(module, 'A')) class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): @@ -102,24 +111,28 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): def test_is_submodule_resource(self): submodule = import_module('ziptestdata.subdirectory') - self.assertTrue(resources.is_resource(submodule, 'binary.file')) + with util.suppress_known_deprecation(): + self.assertTrue(resources.is_resource(submodule, 'binary.file')) def test_read_submodule_resource_by_name(self): - self.assertTrue( - resources.is_resource('ziptestdata.subdirectory', 'binary.file') - ) + with util.suppress_known_deprecation(): + self.assertTrue( + resources.is_resource('ziptestdata.subdirectory', 'binary.file') + ) def test_submodule_contents(self): submodule = import_module('ziptestdata.subdirectory') - self.assertEqual( - set(resources.contents(submodule)), {'__init__.py', 'binary.file'} - ) + with util.suppress_known_deprecation(): + self.assertEqual( + set(resources.contents(submodule)), {'__init__.py', 'binary.file'} + ) def test_submodule_contents_by_name(self): - self.assertEqual( - set(resources.contents('ziptestdata.subdirectory')), - {'__init__.py', 'binary.file'}, - ) + with util.suppress_known_deprecation(): + self.assertEqual( + set(resources.contents('ziptestdata.subdirectory')), + {'__init__.py', 'binary.file'}, + ) class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): @@ -130,12 +143,16 @@ def test_unrelated_contents(self): Test thata zip with two unrelated subpackages return distinct resources. Ref python/importlib_resources#44. """ - self.assertEqual( - set(resources.contents('ziptestdata.one')), {'__init__.py', 'resource1.txt'} - ) - self.assertEqual( - set(resources.contents('ziptestdata.two')), {'__init__.py', 'resource2.txt'} - ) + with util.suppress_known_deprecation(): + self.assertEqual( + set(resources.contents('ziptestdata.one')), + {'__init__.py', 'resource1.txt'}, + ) + with util.suppress_known_deprecation(): + self.assertEqual( + set(resources.contents('ziptestdata.two')), + {'__init__.py', 'resource2.txt'}, + ) class DeletingZipsTest(unittest.TestCase): @@ -176,17 +193,20 @@ def tearDown(self): pass def test_contents_does_not_keep_open(self): - c = resources.contents('ziptestdata') + with util.suppress_known_deprecation(): + c = resources.contents('ziptestdata') self.zip_path.unlink() del c def test_is_resource_does_not_keep_open(self): - c = resources.is_resource('ziptestdata', 'binary.file') + with util.suppress_known_deprecation(): + c = resources.is_resource('ziptestdata', 'binary.file') self.zip_path.unlink() del c def test_is_resource_failure_does_not_keep_open(self): - c = resources.is_resource('ziptestdata', 'not-present') + with util.suppress_known_deprecation(): + c = resources.is_resource('ziptestdata', 'not-present') self.zip_path.unlink() del c @@ -199,17 +219,20 @@ def test_path_does_not_keep_open(self): def test_entered_path_does_not_keep_open(self): # This is what certifi does on import to make its bundle # available for the process duration. - c = resources.path('ziptestdata', 'binary.file').__enter__() + with util.suppress_known_deprecation(): + c = resources.path('ziptestdata', 'binary.file').__enter__() self.zip_path.unlink() del c def test_read_binary_does_not_keep_open(self): - c = resources.read_binary('ziptestdata', 'binary.file') + with util.suppress_known_deprecation(): + c = resources.read_binary('ziptestdata', 'binary.file') self.zip_path.unlink() del c def test_read_text_does_not_keep_open(self): - c = resources.read_text('ziptestdata', 'utf-8.file', encoding='utf-8') + with util.suppress_known_deprecation(): + c = resources.read_text('ziptestdata', 'utf-8.file', encoding='utf-8') self.zip_path.unlink() del c @@ -226,15 +249,18 @@ def tearDownClass(cls): sys.path.remove(cls.site_dir) def test_is_submodule_resource(self): - self.assertTrue( - resources.is_resource(import_module('namespacedata01'), 'binary.file') - ) + with util.suppress_known_deprecation(): + self.assertTrue( + resources.is_resource(import_module('namespacedata01'), 'binary.file') + ) def test_read_submodule_resource_by_name(self): - self.assertTrue(resources.is_resource('namespacedata01', 'binary.file')) + with util.suppress_known_deprecation(): + self.assertTrue(resources.is_resource('namespacedata01', 'binary.file')) def test_submodule_contents(self): - contents = set(resources.contents(import_module('namespacedata01'))) + with util.suppress_known_deprecation(): + contents = set(resources.contents(import_module('namespacedata01'))) try: contents.remove('__pycache__') except KeyError: @@ -242,7 +268,8 @@ def test_submodule_contents(self): self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) def test_submodule_contents_by_name(self): - contents = set(resources.contents('namespacedata01')) + with util.suppress_known_deprecation(): + contents = set(resources.contents('namespacedata01')) try: contents.remove('__pycache__') except KeyError: diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 6ac4332e..b578de9e 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -1,8 +1,10 @@ import abc +import contextlib import importlib import io import sys import types +import warnings from pathlib import Path, PurePath from . import data01 @@ -67,6 +69,13 @@ def create_package(file=None, path=None, is_package=True, contents=()): ) +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + class CommonTests(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. From ed3b2d30e01121b27a6e0d0a8a478a483e437736 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Oct 2021 18:30:27 -0400 Subject: [PATCH 104/496] Add section to the docs guiding the user on how to migrate (relying on the _legacy module as a guide because it's so easy to read). --- CHANGES.rst | 4 +++- docs/using.rst | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index bfe4f49f..f58e2f23 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,9 @@ v5.3.0 * #80: Now raise a ``DeprecationWarning`` for all legacy functions. Instead, users should rely on the ``files()`` - API introduced in importlib_resources 1.3. + API introduced in importlib_resources 1.3. See + `Migrating from Legacy `_ + for guidance on avoiding the deprecated functions. v5.2.3 ====== diff --git a/docs/using.rst b/docs/using.rst index c0a60ad1..ec7f0fad 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -163,6 +163,21 @@ manager. Both relative and absolute paths work for Python 3.7 and newer. +Migrating from Legacy +===================== + +Starting with Python 3.9 and ``importlib_resources`` 1.4, this package +introduced the ``files()`` API, to be preferred over the legacy API, +i.e. the functions ``open_binary``, ``open_text``, ``path``, +``contents``, ``read_text``, ``read_binary``, and ``is_resource``. + +To port to the ``files()`` API, refer to the +`_legacy module `_ +to see simple wrappers that enable drop-in replacement based on the +preferred API, and either copy those or adapt the usage to utilize the +``files`` and ``Traversable`` interfaces directly. + + Extending ========= From 239ae6d56ae545534cf69dc1c91000c620a5f8f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Oct 2021 13:54:29 -0400 Subject: [PATCH 105/496] Provide references to relevant docs and protocol definition for ease-of-use. --- docs/using.rst | 4 +++- importlib_resources/_legacy.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index ec7f0fad..548fa58e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -175,7 +175,9 @@ To port to the ``files()`` API, refer to the `_legacy module `_ to see simple wrappers that enable drop-in replacement based on the preferred API, and either copy those or adapt the usage to utilize the -``files`` and ``Traversable`` interfaces directly. +``files`` and +`Traversable `_ +interfaces directly. Extending diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 7526d512..477f89e4 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -16,7 +16,9 @@ def deprecated(func): @functools.wraps(func) def wrapper(*args, **kwargs): warnings.warn( - f"{func.__name__} is deprecated. Use files() instead.", + f"{func.__name__} is deprecated. Use files() instead. " + "Refer to https://importlib-resources.readthedocs.io" + "/en/latest/using.html#migrating-from-legacy for migration advice.", DeprecationWarning, stacklevel=2, ) From 57d4e211f38cd3d0e0e5b4b997a77d63b1979c95 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Oct 2021 14:12:41 -0400 Subject: [PATCH 106/496] Update test_open_binary to match upstream implementation. --- importlib_resources/tests/test_open.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 1c8a943a..28072366 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -22,9 +22,9 @@ def execute(self, package, path): class OpenTests: def test_open_binary(self): with util.suppress_known_deprecation(): - with resources.open_binary(self.data, 'utf-8.file') as fp: + with resources.open_binary(self.data, 'binary.file') as fp: result = fp.read() - self.assertEqual(result, b'Hello, UTF-8 world!\n') + self.assertEqual(result, b'\x00\x01\x02\x03') def test_open_text_default_encoding(self): with util.suppress_known_deprecation(): From 329ae9d5f2c570325ae38ccae1e8b55bd4dc9f0a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 7 Oct 2021 01:13:48 +0200 Subject: [PATCH 107/496] Fix typos in the Lib directory (GH-28775) Fix typos in the Lib directory as identified by codespell. Co-authored-by: Terry Jan Reedy --- importlib_resources/_adapters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/_adapters.py b/importlib_resources/_adapters.py index 9907b148..ea363d86 100644 --- a/importlib_resources/_adapters.py +++ b/importlib_resources/_adapters.py @@ -41,8 +41,8 @@ def _io_wrapper(file, mode='r', *args, **kwargs): class CompatibilityFiles: """ - Adapter for an existing or non-existant resource reader - to provide a compability .files(). + Adapter for an existing or non-existent resource reader + to provide a compatibility .files(). """ class SpecPath(abc.Traversable): @@ -83,7 +83,7 @@ def open(self, mode='r', *args, **kwargs): class ChildPath(abc.Traversable): """ Path tied to a resource reader child. - Can be read but doesn't expose any meaningfull children. + Can be read but doesn't expose any meaningful children. """ def __init__(self, reader, name): From 020b3f88007b71c3414d15f8700e90df8fde8f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Tue, 19 Oct 2021 01:07:17 +0100 Subject: [PATCH 108/496] abc: avoid abbreviations in the Traversable documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 56dc8127..d39dc1ad 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -76,7 +76,7 @@ def read_text(self, encoding=None): @abc.abstractmethod def is_dir(self) -> bool: """ - Return True if self is a dir + Return True if self is a directory """ @abc.abstractmethod From 550ee518136342b156d8fcd7691542d15ba351fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Oct 2021 22:28:46 -0400 Subject: [PATCH 109/496] Remove references to specific Python versions, as this project and CPython are evolving over time. --- docs/index.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 82cb007a..798da5c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,11 +11,13 @@ importable `Python package`_. Resources can live on the file system or in a zip file, with support for other loader_ classes that implement the appropriate API for reading resources. -``importlib_resources`` is a backport of Python 3.9's standard library -`importlib.resources`_ module for Python 2.7, and 3.5 through 3.8. Users of -Python 3.9 and beyond are encouraged to use the standard library module. -Developers looking for detailed API descriptions should refer to the Python -3.9 standard library documentation. +``importlib_resources`` supplies a backport of +:doc:`importlib.resources `, +enabling early access to features of future Python versions and making +functionality available for older Python versions. Users are encouraged to +use the Python standard library where suitable and fall back to +this library for future compatibility. Developers looking for detailed API +descriptions should refer to the standard library documentation. The documentation here includes a general :ref:`usage ` guide and a :ref:`migration ` guide for projects that want to adopt @@ -39,7 +41,6 @@ Indices and tables * :ref:`search` -.. _`importlib.resources`: https://docs.python.org/3.7/library/importlib.html#module-importlib.resources .. _`Basic Resource Access`: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access .. _`Python package`: https://docs.python.org/3/reference/import.html#packages .. _loader: https://docs.python.org/3/reference/import.html#finders-and-loaders From c0a186d4df40ee37687688b3151f9f0a340f0ed4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 21:13:50 -0400 Subject: [PATCH 110/496] Port test_contents to files API --- importlib_resources/tests/test_contents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index c95e1a77..525568e8 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -15,8 +15,8 @@ class ContentsTests: } def test_contents(self): - with util.suppress_known_deprecation(): - assert self.expected <= set(resources.contents(self.data)) + contents = {path.name for path in resources.files(self.data).iterdir()} + assert self.expected <= contents class ContentsDiskTests(ContentsTests, unittest.TestCase): From bba97e5a3b1d92b39976eead7a02cae37315fab4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 21:45:53 -0400 Subject: [PATCH 111/496] Remove tests for relative/absolute paths, no longer relevant. Move normalize_path to _legacy. --- importlib_resources/_common.py | 15 +-------------- importlib_resources/_legacy.py | 25 +++++++++++++++++++------ importlib_resources/tests/util.py | 12 ------------ 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 25511672..24df8d39 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Any, Optional +from typing import Union, Optional from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -23,19 +23,6 @@ def files(package): return from_package(get_package(package)) -def normalize_path(path): - # type: (Any) -> str - """Normalize a path by ensuring it is a string. - - If the resulting string contains path separators, an exception is raised. - """ - str_path = str(path) - parent, file_name = os.path.split(str_path) - if parent: - raise ValueError(f'{path!r} must be only a file name') - return file_name - - def get_resource_reader(package): # type: (types.ModuleType) -> Optional[ResourceReader] """ diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 477f89e4..caa7078f 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -4,7 +4,7 @@ import types import warnings -from typing import Union, Iterable, ContextManager, BinaryIO, TextIO +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any from . import _common @@ -27,16 +27,29 @@ def wrapper(*args, **kwargs): return wrapper +def normalize_path(path): + # type: (Any) -> str + """Normalize a path by ensuring it is a string. + + If the resulting string contains path separators, an exception is raised. + """ + str_path = str(path) + parent, file_name = os.path.split(str_path) + if parent: + raise ValueError(f'{path!r} must be only a file name') + return file_name + + @deprecated def open_binary(package: Package, resource: Resource) -> BinaryIO: """Return a file-like object opened for binary reading of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).open('rb') + return (_common.files(package) / normalize_path(resource)).open('rb') @deprecated def read_binary(package: Package, resource: Resource) -> bytes: """Return the binary contents of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).read_bytes() + return (_common.files(package) / normalize_path(resource)).read_bytes() @deprecated @@ -47,7 +60,7 @@ def open_text( errors: str = 'strict', ) -> TextIO: """Return a file-like object opened for text reading of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).open( + return (_common.files(package) / normalize_path(resource)).open( 'r', encoding=encoding, errors=errors ) @@ -85,7 +98,7 @@ def is_resource(package: Package, name: str) -> bool: Directories are *not* resources. """ - resource = _common.normalize_path(name) + resource = normalize_path(name) return any( traversable.name == resource and traversable.is_file() for traversable in _common.files(package).iterdir() @@ -105,4 +118,4 @@ def path( raised if the file was deleted prior to the context manager exiting). """ - return _common.as_file(_common.files(package) / _common.normalize_path(resource)) + return _common.as_file(_common.files(package) / normalize_path(resource)) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index b578de9e..e33b32b1 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -106,18 +106,6 @@ def test_pathlib_path(self): path = PurePath('utf-8.file') self.execute(data01, path) - def test_absolute_path(self): - # An absolute path is a ValueError. - path = Path(__file__) - full_path = path.parent / 'utf-8.file' - with self.assertRaises(ValueError): - self.execute(data01, full_path) - - def test_relative_path(self): - # A reative path is a ValueError. - with self.assertRaises(ValueError): - self.execute(data01, '../data01/utf-8.file') - def test_importing_module_as_side_effect(self): # The anchor package can already be imported. del sys.modules[data01.__name__] From eda56b310ae801c6778ba1e16cf1bc56a5eb8ed6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 21:30:02 -0400 Subject: [PATCH 112/496] Port test_open to traversable API. --- importlib_resources/tests/test_open.py | 57 +++++++++++--------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 28072366..87b42c3d 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -7,47 +7,44 @@ class CommonBinaryTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with util.suppress_known_deprecation(): - with resources.open_binary(package, path): - pass + target = resources.files(package).joinpath(path) + with target.open('rb'): + pass class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with util.suppress_known_deprecation(): - with resources.open_text(package, path): - pass + target = resources.files(package).joinpath(path) + with target.open(): + pass class OpenTests: def test_open_binary(self): - with util.suppress_known_deprecation(): - with resources.open_binary(self.data, 'binary.file') as fp: - result = fp.read() - self.assertEqual(result, b'\x00\x01\x02\x03') + target = resources.files(self.data) / 'binary.file' + with target.open('rb') as fp: + result = fp.read() + self.assertEqual(result, b'\x00\x01\x02\x03') def test_open_text_default_encoding(self): - with util.suppress_known_deprecation(): - with resources.open_text(self.data, 'utf-8.file') as fp: - result = fp.read() + target = resources.files(self.data) / 'utf-8.file' + with target.open() as fp: + result = fp.read() self.assertEqual(result, 'Hello, UTF-8 world!\n') def test_open_text_given_encoding(self): - with util.suppress_known_deprecation(): - with resources.open_text( - self.data, 'utf-16.file', 'utf-16', 'strict' - ) as fp: - result = fp.read() + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-16', errors='strict') as fp: + result = fp.read() self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_open_text_with_errors(self): # Raises UnicodeError without the 'errors' argument. - with util.suppress_known_deprecation(): - with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'strict') as fp: - self.assertRaises(UnicodeError, fp.read) - with util.suppress_known_deprecation(): - with resources.open_text(self.data, 'utf-16.file', 'utf-8', 'ignore') as fp: - result = fp.read() + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-8', errors='strict') as fp: + self.assertRaises(UnicodeError, fp.read) + with target.open(encoding='utf-8', errors='ignore') as fp: + result = fp.read() self.assertEqual( result, 'H\x00e\x00l\x00l\x00o\x00,\x00 ' @@ -56,16 +53,12 @@ def test_open_text_with_errors(self): ) def test_open_binary_FileNotFoundError(self): - with util.suppress_known_deprecation(): - self.assertRaises( - FileNotFoundError, resources.open_binary, self.data, 'does-not-exist' - ) + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open, 'rb') def test_open_text_FileNotFoundError(self): - with util.suppress_known_deprecation(): - self.assertRaises( - FileNotFoundError, resources.open_text, self.data, 'does-not-exist' - ) + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open) class OpenDiskTests(OpenTests, unittest.TestCase): From d87a8dde6baf68fc6f3afa730925c29bea477dd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 22:01:06 -0400 Subject: [PATCH 113/496] Port test_path to traversable API. --- importlib_resources/tests/test_path.py | 31 +++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index af433ce0..4f4d3943 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -8,9 +8,8 @@ class CommonTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with util.suppress_known_deprecation(): - with resources.path(package, path): - pass + with resources.as_file(resources.files(package).joinpath(path)): + pass class PathTests: @@ -18,13 +17,13 @@ def test_reading(self): # Path should be readable. # Test also implicitly verifies the returned object is a pathlib.Path # instance. - with util.suppress_known_deprecation(): - with resources.path(self.data, 'utf-8.file') as path: - self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) - # pathlib.Path.read_text() was introduced in Python 3.5. - with path.open('r', encoding='utf-8') as file: - text = file.read() - self.assertEqual('Hello, UTF-8 world!\n', text) + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) + # pathlib.Path.read_text() was introduced in Python 3.5. + with path.open('r', encoding='utf-8') as file: + text = file.read() + self.assertEqual('Hello, UTF-8 world!\n', text) class PathDiskTests(PathTests, unittest.TestCase): @@ -36,9 +35,9 @@ def test_natural_path(self): file-system-backed resources do not get the tempdir treatment. """ - with util.suppress_known_deprecation(): - with resources.path(self.data, 'utf-8.file') as path: - assert 'data' in str(path) + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + assert 'data' in str(path) class PathMemoryTests(PathTests, unittest.TestCase): @@ -56,9 +55,9 @@ class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): def test_remove_in_context_manager(self): # It is not an error if the file that was temporarily stashed on the # file system is removed inside the `with` stanza. - with util.suppress_known_deprecation(): - with resources.path(self.data, 'utf-8.file') as path: - path.unlink() + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + path.unlink() if __name__ == '__main__': From 6f35cd3cd9c88ae88a2a8278032a660b9bdbe44f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 22:06:58 -0400 Subject: [PATCH 114/496] Port test_read to traversable API. --- importlib_resources/tests/test_read.py | 40 ++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index da7d2745..41dd6db5 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -8,40 +8,36 @@ class CommonBinaryTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with util.suppress_known_deprecation(): - resources.read_binary(package, path) + resources.files(package).joinpath(path).read_bytes() class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - with util.suppress_known_deprecation(): - resources.read_text(package, path) + resources.files(package).joinpath(path).read_text() class ReadTests: - def test_read_binary(self): - with util.suppress_known_deprecation(): - result = resources.read_binary(self.data, 'binary.file') + def test_read_bytes(self): + result = resources.files(self.data).joinpath('binary.file').read_bytes() self.assertEqual(result, b'\0\1\2\3') def test_read_text_default_encoding(self): - with util.suppress_known_deprecation(): - result = resources.read_text(self.data, 'utf-8.file') + result = resources.files(self.data).joinpath('utf-8.file').read_text() self.assertEqual(result, 'Hello, UTF-8 world!\n') def test_read_text_given_encoding(self): - with util.suppress_known_deprecation(): - result = resources.read_text(self.data, 'utf-16.file', encoding='utf-16') + result = ( + resources.files(self.data) + .joinpath('utf-16.file') + .read_text(encoding='utf-16') + ) self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_read_text_with_errors(self): # Raises UnicodeError without the 'errors' argument. - with util.suppress_known_deprecation(): - self.assertRaises( - UnicodeError, resources.read_text, self.data, 'utf-16.file' - ) - with util.suppress_known_deprecation(): - result = resources.read_text(self.data, 'utf-16.file', errors='ignore') + target = resources.files(self.data) / 'utf-16.file' + self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') + result = target.read_text(encoding='utf-8', errors='ignore') self.assertEqual( result, 'H\x00e\x00l\x00l\x00o\x00,\x00 ' @@ -57,13 +53,15 @@ class ReadDiskTests(ReadTests, unittest.TestCase): class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('ziptestdata.subdirectory') - with util.suppress_known_deprecation(): - result = resources.read_binary(submodule, 'binary.file') + result = resources.files(submodule).joinpath('binary.file').read_bytes() self.assertEqual(result, b'\0\1\2\3') def test_read_submodule_resource_by_name(self): - with util.suppress_known_deprecation(): - result = resources.read_binary('ziptestdata.subdirectory', 'binary.file') + result = ( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .read_bytes() + ) self.assertEqual(result, b'\0\1\2\3') From 797c097acfb09a37dc0851e0a8c648967bc14dff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 22:36:29 -0400 Subject: [PATCH 115/496] Port test_resource to traversable API. --- importlib_resources/tests/test_resource.py | 165 +++++++++------------ 1 file changed, 68 insertions(+), 97 deletions(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 9fcf6705..5affd8b0 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -14,38 +14,18 @@ class ResourceTests: # Subclasses are expected to set the `data` attribute. - def test_is_resource_good_path(self): - with util.suppress_known_deprecation(): - self.assertTrue(resources.is_resource(self.data, 'binary.file')) - - def test_is_resource_missing(self): - with util.suppress_known_deprecation(): - self.assertFalse(resources.is_resource(self.data, 'not-a-file')) - - def test_is_resource_subresource_directory(self): - # Directories are not resources. - with util.suppress_known_deprecation(): - self.assertFalse(resources.is_resource(self.data, 'subdirectory')) - - def test_contents(self): - with util.suppress_known_deprecation(): - contents = set(resources.contents(self.data)) - # There may be cruft in the directory listing of the data directory. - # It could have a __pycache__ directory, - # an artifact of the - # test suite importing these modules, which - # are not germane to this test, so just filter them out. - contents.discard('__pycache__') - self.assertEqual( - sorted(contents), - [ - '__init__.py', - 'binary.file', - 'subdirectory', - 'utf-16.file', - 'utf-8.file', - ], - ) + def test_is_file_exists(self): + target = resources.files(self.data) / 'binary.file' + self.assertTrue(target.is_file()) + + def test_is_file_missing(self): + target = resources.files(self.data) / 'not-a-file' + self.assertFalse(target.is_file()) + + def test_is_dir(self): + target = resources.files(self.data) / 'subdirectory' + self.assertFalse(target.is_file()) + self.assertTrue(target.is_dir()) class ResourceDiskTests(ResourceTests, unittest.TestCase): @@ -57,34 +37,34 @@ class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): pass +def names(traversable): + return {item.name for item in traversable.iterdir()} + + class ResourceLoaderTests(unittest.TestCase): def test_resource_contents(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C'] ) - with util.suppress_known_deprecation(): - self.assertEqual(set(resources.contents(package)), {'A', 'B', 'C'}) + self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) - def test_resource_is_resource(self): + def test_is_file(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - with util.suppress_known_deprecation(): - self.assertTrue(resources.is_resource(package, 'B')) + self.assertTrue(resources.files(package).joinpath('B').is_file()) - def test_resource_directory_is_not_resource(self): + def test_is_dir(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - with util.suppress_known_deprecation(): - self.assertFalse(resources.is_resource(package, 'D')) + self.assertTrue(resources.files(package).joinpath('D').is_dir()) - def test_resource_missing_is_not_resource(self): + def test_resource_missing(self): package = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] ) - with util.suppress_known_deprecation(): - self.assertFalse(resources.is_resource(package, 'Z')) + self.assertFalse(resources.files(package).joinpath('Z').is_file()) class ResourceCornerCaseTests(unittest.TestCase): @@ -102,8 +82,7 @@ def test_package_has_no_reader_fallback(self): module.__file__ = '/path/which/shall/not/be/named' module.__spec__.loader = module.__loader__ module.__spec__.origin = module.__file__ - with util.suppress_known_deprecation(): - self.assertFalse(resources.is_resource(module, 'A')) + self.assertFalse(resources.files(module).joinpath('A').is_file()) class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): @@ -111,28 +90,26 @@ class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): def test_is_submodule_resource(self): submodule = import_module('ziptestdata.subdirectory') - with util.suppress_known_deprecation(): - self.assertTrue(resources.is_resource(submodule, 'binary.file')) + self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) def test_read_submodule_resource_by_name(self): - with util.suppress_known_deprecation(): - self.assertTrue( - resources.is_resource('ziptestdata.subdirectory', 'binary.file') - ) + self.assertTrue( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .is_file() + ) def test_submodule_contents(self): submodule = import_module('ziptestdata.subdirectory') - with util.suppress_known_deprecation(): - self.assertEqual( - set(resources.contents(submodule)), {'__init__.py', 'binary.file'} - ) + self.assertEqual( + names(resources.files(submodule)), {'__init__.py', 'binary.file'} + ) def test_submodule_contents_by_name(self): - with util.suppress_known_deprecation(): - self.assertEqual( - set(resources.contents('ziptestdata.subdirectory')), - {'__init__.py', 'binary.file'}, - ) + self.assertEqual( + names(resources.files('ziptestdata.subdirectory')), + {'__init__.py', 'binary.file'}, + ) class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): @@ -143,16 +120,14 @@ def test_unrelated_contents(self): Test thata zip with two unrelated subpackages return distinct resources. Ref python/importlib_resources#44. """ - with util.suppress_known_deprecation(): - self.assertEqual( - set(resources.contents('ziptestdata.one')), - {'__init__.py', 'resource1.txt'}, - ) - with util.suppress_known_deprecation(): - self.assertEqual( - set(resources.contents('ziptestdata.two')), - {'__init__.py', 'resource2.txt'}, - ) + self.assertEqual( + names(resources.files('ziptestdata.one')), + {'__init__.py', 'resource1.txt'}, + ) + self.assertEqual( + names(resources.files('ziptestdata.two')), + {'__init__.py', 'resource2.txt'}, + ) class DeletingZipsTest(unittest.TestCase): @@ -192,47 +167,43 @@ def tearDown(self): # If the test fails, this will probably fail too pass - def test_contents_does_not_keep_open(self): - with util.suppress_known_deprecation(): - c = resources.contents('ziptestdata') + def test_iterdir_does_not_keep_open(self): + c = [item.name for item in resources.files('ziptestdata').iterdir()] self.zip_path.unlink() del c - def test_is_resource_does_not_keep_open(self): - with util.suppress_known_deprecation(): - c = resources.is_resource('ziptestdata', 'binary.file') + def test_is_file_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('binary.file').is_file() self.zip_path.unlink() del c - def test_is_resource_failure_does_not_keep_open(self): - with util.suppress_known_deprecation(): - c = resources.is_resource('ziptestdata', 'not-present') + def test_is_file_failure_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('not-present').is_file() self.zip_path.unlink() del c @unittest.skip("Desired but not supported.") - def test_path_does_not_keep_open(self): - c = resources.path('ziptestdata', 'binary.file') + def test_as_file_does_not_keep_open(self): # pragma: no cover + c = resources.as_file(resources.files('ziptestdata') / 'binary.file') self.zip_path.unlink() del c def test_entered_path_does_not_keep_open(self): # This is what certifi does on import to make its bundle # available for the process duration. - with util.suppress_known_deprecation(): - c = resources.path('ziptestdata', 'binary.file').__enter__() + c = resources.as_file( + resources.files('ziptestdata') / 'binary.file' + ).__enter__() self.zip_path.unlink() del c def test_read_binary_does_not_keep_open(self): - with util.suppress_known_deprecation(): - c = resources.read_binary('ziptestdata', 'binary.file') + c = resources.files('ziptestdata').joinpath('binary.file').read_bytes() self.zip_path.unlink() del c def test_read_text_does_not_keep_open(self): - with util.suppress_known_deprecation(): - c = resources.read_text('ziptestdata', 'utf-8.file', encoding='utf-8') + c = resources.files('ziptestdata').joinpath('utf-8.file').read_text() self.zip_path.unlink() del c @@ -249,18 +220,19 @@ def tearDownClass(cls): sys.path.remove(cls.site_dir) def test_is_submodule_resource(self): - with util.suppress_known_deprecation(): - self.assertTrue( - resources.is_resource(import_module('namespacedata01'), 'binary.file') - ) + self.assertTrue( + resources.files(import_module('namespacedata01')) + .joinpath('binary.file') + .is_file() + ) def test_read_submodule_resource_by_name(self): - with util.suppress_known_deprecation(): - self.assertTrue(resources.is_resource('namespacedata01', 'binary.file')) + self.assertTrue( + resources.files('namespacedata01').joinpath('binary.file').is_file() + ) def test_submodule_contents(self): - with util.suppress_known_deprecation(): - contents = set(resources.contents(import_module('namespacedata01'))) + contents = names(resources.files(import_module('namespacedata01'))) try: contents.remove('__pycache__') except KeyError: @@ -268,8 +240,7 @@ def test_submodule_contents(self): self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) def test_submodule_contents_by_name(self): - with util.suppress_known_deprecation(): - contents = set(resources.contents('namespacedata01')) + contents = names(resources.files('namespacedata01')) try: contents.remove('__pycache__') except KeyError: From dfe87a6b4712d82dbf2ef8fed8a534058de9d95d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 22:37:05 -0400 Subject: [PATCH 116/496] Remove 'suppress_known_deprecation' helper. --- importlib_resources/tests/util.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index e33b32b1..c6d83e4b 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -1,10 +1,8 @@ import abc -import contextlib import importlib import io import sys import types -import warnings from pathlib import Path, PurePath from . import data01 @@ -69,13 +67,6 @@ def create_package(file=None, path=None, is_package=True, contents=()): ) -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - class CommonTests(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. From 5869cf323fae4fe45160801695a7938efe995339 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Oct 2021 22:44:20 -0400 Subject: [PATCH 117/496] Exclude legacy from coverage. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 8e164f12..b3df18a1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* */_itertools.py + */_legacy.py [report] show_missing = True From aae281a9ff6c9a1fa9daad82c79457e8770a1c7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Oct 2021 14:19:58 -0400 Subject: [PATCH 118/496] 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 64d03de94678a394c71635e7d1ae67ab425a476e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Oct 2021 18:41:31 -0400 Subject: [PATCH 119/496] Move normalize_path to _legacy --- importlib_resources/_common.py | 15 +-------------- importlib_resources/_legacy.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 25511672..24df8d39 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Any, Optional +from typing import Union, Optional from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -23,19 +23,6 @@ def files(package): return from_package(get_package(package)) -def normalize_path(path): - # type: (Any) -> str - """Normalize a path by ensuring it is a string. - - If the resulting string contains path separators, an exception is raised. - """ - str_path = str(path) - parent, file_name = os.path.split(str_path) - if parent: - raise ValueError(f'{path!r} must be only a file name') - return file_name - - def get_resource_reader(package): # type: (types.ModuleType) -> Optional[ResourceReader] """ diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 477f89e4..caa7078f 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -4,7 +4,7 @@ import types import warnings -from typing import Union, Iterable, ContextManager, BinaryIO, TextIO +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any from . import _common @@ -27,16 +27,29 @@ def wrapper(*args, **kwargs): return wrapper +def normalize_path(path): + # type: (Any) -> str + """Normalize a path by ensuring it is a string. + + If the resulting string contains path separators, an exception is raised. + """ + str_path = str(path) + parent, file_name = os.path.split(str_path) + if parent: + raise ValueError(f'{path!r} must be only a file name') + return file_name + + @deprecated def open_binary(package: Package, resource: Resource) -> BinaryIO: """Return a file-like object opened for binary reading of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).open('rb') + return (_common.files(package) / normalize_path(resource)).open('rb') @deprecated def read_binary(package: Package, resource: Resource) -> bytes: """Return the binary contents of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).read_bytes() + return (_common.files(package) / normalize_path(resource)).read_bytes() @deprecated @@ -47,7 +60,7 @@ def open_text( errors: str = 'strict', ) -> TextIO: """Return a file-like object opened for text reading of the resource.""" - return (_common.files(package) / _common.normalize_path(resource)).open( + return (_common.files(package) / normalize_path(resource)).open( 'r', encoding=encoding, errors=errors ) @@ -85,7 +98,7 @@ def is_resource(package: Package, name: str) -> bool: Directories are *not* resources. """ - resource = _common.normalize_path(name) + resource = normalize_path(name) return any( traversable.name == resource and traversable.is_file() for traversable in _common.files(package).iterdir() @@ -105,4 +118,4 @@ def path( raised if the file was deleted prior to the context manager exiting). """ - return _common.as_file(_common.files(package) / _common.normalize_path(resource)) + return _common.as_file(_common.files(package) / normalize_path(resource)) From 04973b10912ed565e32e22aacfa3835fbadd3c9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Oct 2021 18:43:13 -0400 Subject: [PATCH 120/496] Move definition of Resource to _legacy. --- importlib_resources/__init__.py | 2 +- importlib_resources/_common.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 2468f575..15f6b26b 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -4,7 +4,6 @@ as_file, files, Package, - Resource, ) from ._legacy import ( @@ -15,6 +14,7 @@ read_text, is_resource, path, + Resource, ) from importlib_resources.abc import ResourceReader diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 24df8d39..a12e2c75 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -12,7 +12,6 @@ from ._compat import wrap_spec Package = Union[types.ModuleType, str] -Resource = Union[str, os.PathLike] def files(package): From 778e547b0a0f208e20cc94974a37fc6a1f53b28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Fri, 22 Oct 2021 19:51:51 +0100 Subject: [PATCH 121/496] _legacy: fix Resource type hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_resources/_legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index caa7078f..1d5d3f1f 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -9,7 +9,7 @@ from . import _common Package = Union[types.ModuleType, str] -Resource = Union[str, os.PathLike] +Resource = str def deprecated(func): From 15073ca2ac7f26e876c6e6ecacebde285b3cded8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Oct 2021 10:28:31 -0400 Subject: [PATCH 122/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f58e2f23..d908d37a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.4.0 +====== + +* *80: Test suite now relies entirely on the traversable + API. + v5.3.0 ====== From 0019b0af43b9e381e2f0b14753d1bf40ce204490 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 20:08:49 -0500 Subject: [PATCH 123/496] 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 124/496] 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 3beb2fd5831e65f7b45033e1ec276c4a6b4ca973 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Dec 2021 23:20:20 -0500 Subject: [PATCH 125/496] Bump version for Python 3.11 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 29aef77d..e9eb5993 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ were contributed to different versions in the standard library: * - importlib_resources - stdlib - * - 5.2 + * - 5.4 - 3.11 * - 5.0 - 3.10 From b1183751f0e4ef1f262e90e95077aa98b4280b75 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 14 Jan 2022 14:01:32 -0500 Subject: [PATCH 126/496] fix(types): Adding some typing from typeshed --- importlib_resources/abc.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index d39dc1ad..9b689b76 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,9 +1,18 @@ import abc -from typing import BinaryIO, Iterable, Text +from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text +from io import BufferedReader +from typing import Union, Optional +from os import PathLike from ._compat import runtime_checkable, Protocol +__all__ = ["ResourceReader", "Traversable", "TraversableResources"] + + +StrPath = Union[str, PathLike[str]] + + class ResourceReader(metaclass=abc.ABCMeta): """Abstract base class for loaders to provide resource reading support.""" @@ -54,19 +63,19 @@ class Traversable(Protocol): """ @abc.abstractmethod - def iterdir(self): + def iterdir(self) -> Iterator["Traversable"]: """ Yield Traversable objects in self """ - def read_bytes(self): + def read_bytes(self) -> bytes: """ Read contents of self as bytes """ with self.open('rb') as strm: return strm.read() - def read_text(self, encoding=None): + def read_text(self, encoding: Optional[str] = None) -> str: """ Read contents of self as text """ @@ -86,12 +95,12 @@ def is_file(self) -> bool: """ @abc.abstractmethod - def joinpath(self, child): + def joinpath(self, child: StrPath) -> "Traversable": """ Return Traversable child in self """ - def __truediv__(self, child): + def __truediv__(self, child: StrPath) -> "Traversable": """ Return Traversable child in self """ @@ -121,17 +130,17 @@ class TraversableResources(ResourceReader): """ @abc.abstractmethod - def files(self): + def files(self) -> "Traversable": """Return a Traversable object for the loaded package.""" - def open_resource(self, resource): + def open_resource(self, resource: StrPath) -> BufferedReader: return self.files().joinpath(resource).open('rb') - def resource_path(self, resource): + def resource_path(self, resource: Any) -> NoReturn: raise FileNotFoundError(resource) - def is_resource(self, path): + def is_resource(self, path: StrPath) -> bool: return self.files().joinpath(path).is_file() - def contents(self): + def contents(self) -> Iterator[str]: return (item.name for item in self.files().iterdir()) From 963f6f354c5a2b9e8c4fbbb9616f9f99838cf227 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 18 Jan 2022 17:44:06 -0500 Subject: [PATCH 127/496] Update importlib_resources/abc.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Filipe Laíns --- importlib_resources/abc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 9b689b76..58f5178e 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -10,7 +10,8 @@ __all__ = ["ResourceReader", "Traversable", "TraversableResources"] -StrPath = Union[str, PathLike[str]] +# PathLike is only subscriptable at runtime in 3.9+ +StrPath = Union[str, "PathLike[str]"] class ResourceReader(metaclass=abc.ABCMeta): From 4f9825dafa8d13a5f8b8bd8eb8bfc6414329cb18 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 04:09:17 -0500 Subject: [PATCH 128/496] 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 129/496] 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 8949d1a1169c9271ceb8aab3f1deea9d82e2fa0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 21:45:52 -0500 Subject: [PATCH 130/496] 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 131/496] 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 132/496] 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 b0968d59009bcf6f8bd7d21e608aebf92d64ffcd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 20:52:15 -0500 Subject: [PATCH 133/496] ValueError is the exception that needs suppressing. Fixes #243. --- importlib_resources/tests/update-zips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/tests/update-zips.py b/importlib_resources/tests/update-zips.py index 9ef0224c..231334aa 100755 --- a/importlib_resources/tests/update-zips.py +++ b/importlib_resources/tests/update-zips.py @@ -42,7 +42,7 @@ def generate(suffix): def walk(datapath): for dirpath, dirnames, filenames in os.walk(datapath): - with contextlib.suppress(KeyError): + with contextlib.suppress(ValueError): dirnames.remove('__pycache__') for filename in filenames: res = pathlib.Path(dirpath) / filename From 2324e57b6f24f3ce7c2c7bbf8520c6bd11c40b0a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 20:54:15 -0500 Subject: [PATCH 134/496] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d908d37a..4c40d4cf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.5.0 +====== + +* Require Python 3.7 or later. +* #243: Fix error when no ``__pycache__`` directories exist + when testing ``update-zips``. + v5.4.0 ====== From d95e4e21cfb221cb71dd53e782a47fc6555a3bcc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 20:56:19 -0500 Subject: [PATCH 135/496] Update changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4c40d4cf..ed1be905 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,7 @@ v5.5.0 v5.4.0 ====== -* *80: Test suite now relies entirely on the traversable +* #80: Test suite now relies entirely on the traversable API. v5.3.0 From a9ea801a43fc62a569cf60e1c28e477ba510d8a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 21:58:57 -0500 Subject: [PATCH 136/496] 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 137/496] 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 138/496] 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 8f8d7e03a3a91a5a82cff96de22f20be0b77f408 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Mar 2022 19:57:38 -0400 Subject: [PATCH 139/496] Move StrPath to _compat module. --- importlib_resources/_compat.py | 10 ++++++++++ importlib_resources/abc.py | 10 ++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 61e48d47..8d7ade08 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -1,9 +1,12 @@ # flake8: noqa import abc +import os import sys import pathlib from contextlib import suppress +from typing import Union + if sys.version_info >= (3, 10): from zipfile import Path as ZipPath # type: ignore @@ -96,3 +99,10 @@ def wrap_spec(package): from . import _adapters return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) + + +if sys.version_info >= (3, 9): + StrPath = Union[str, os.PathLike[str]] +else: + # PathLike is only subscriptable at runtime in 3.9+ + StrPath = Union[str, "os.PathLike[str]"] diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 58f5178e..c7e645d1 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,19 +1,13 @@ import abc -from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text +from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional from io import BufferedReader -from typing import Union, Optional -from os import PathLike -from ._compat import runtime_checkable, Protocol +from ._compat import runtime_checkable, Protocol, StrPath __all__ = ["ResourceReader", "Traversable", "TraversableResources"] -# PathLike is only subscriptable at runtime in 3.9+ -StrPath = Union[str, "PathLike[str]"] - - class ResourceReader(metaclass=abc.ABCMeta): """Abstract base class for loaders to provide resource reading support.""" From 2c4a1bd886ea220e8233c1f4c7a13a51c65653af Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Mar 2022 19:59:03 -0400 Subject: [PATCH 140/496] Prefer BufferedReader from namespace. --- importlib_resources/abc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index c7e645d1..a2b0af62 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,6 +1,6 @@ import abc +import io from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional -from io import BufferedReader from ._compat import runtime_checkable, Protocol, StrPath @@ -128,7 +128,7 @@ class TraversableResources(ResourceReader): def files(self) -> "Traversable": """Return a Traversable object for the loaded package.""" - def open_resource(self, resource: StrPath) -> BufferedReader: + def open_resource(self, resource: StrPath) -> io.BufferedReader: return self.files().joinpath(resource).open('rb') def resource_path(self, resource: Any) -> NoReturn: From acb40b621fcb5ca243a04e6fa7b4955967ab0889 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Apr 2022 12:05:05 -0400 Subject: [PATCH 141/496] Update Traversable docs to clarify behavior on joinpath and expectations on exceptions. Update joinpath to accept multiple arguments. Updated simple.ResourceContainer to match the protocol. --- importlib_resources/abc.py | 11 +++++++++-- importlib_resources/simple.py | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index a2b0af62..da69af4f 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -55,6 +55,9 @@ class Traversable(Protocol): """ An object with a subset of pathlib.Path methods suitable for traversing directories and opening files. + + Any exceptions that occur when accessing the backing resource + may propagate unaltered. """ @abc.abstractmethod @@ -90,9 +93,13 @@ def is_file(self) -> bool: """ @abc.abstractmethod - def joinpath(self, child: StrPath) -> "Traversable": + def joinpath(self, *descendants: StrPath) -> "Traversable": """ - Return Traversable child in self + Return Traversable resolved with any descendants applied. + + Each descendant should be a path segment relative to self + and main contain multiple levels separated by + `posixpath.sep` (``/``). """ def __truediv__(self, child: StrPath) -> "Traversable": diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index da073cbd..1f6e43a2 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,10 +99,13 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - def joinpath(self, name): + def joinpath(self, *names): + if not names: + return self + name, rest = names[0], names[1:] return next( traversable for traversable in self.iterdir() if traversable.name == name - ) + ).joinpath(*rest) class TraversableReader(TraversableResources, SimpleReader): From 85840e266b07dea17e439f6f3e148e12b804b0d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 8 Apr 2022 14:11:13 -0400 Subject: [PATCH 142/496] Ignore coverage for simple.py --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index b3df18a1..3b2d64ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ omit = */.tox/* */_itertools.py */_legacy.py + */simple.py [report] show_missing = True From ab11f0117a17f07963a9c705e0e3efed40ba413a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 13 Apr 2022 08:26:49 -0400 Subject: [PATCH 143/496] Correct typo and clarify 'each' in joinpath docstring. --- importlib_resources/abc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index da69af4f..5e38ba61 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -98,8 +98,8 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": Return Traversable resolved with any descendants applied. Each descendant should be a path segment relative to self - and main contain multiple levels separated by - `posixpath.sep` (``/``). + and each may contain multiple levels separated by + ``posixpath.sep`` (``/``). """ def __truediv__(self, child: StrPath) -> "Traversable": From 8a17e814bc44aa2cf43bfee69260d85043870fd3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 13 Apr 2022 08:29:39 -0400 Subject: [PATCH 144/496] Update changelog. Ref #248. --- CHANGES.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3898729..290694e3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v5.7.0 +====== + +* #248: ``abc.Traversable.joinpath`` now allows for multiple + arguments and specifies that ``posixpath.sep`` is allowed + in any argument to accept multiple arguments, matching the + behavior found in ``zipfile.Path`` and ``pathlib.Path``. + + ``simple.ResourceContainer`` now honors this behavior. + v5.6.0 ====== From 75b361320211fe8b6e9fbea5765056b24ffd18ba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 13 Apr 2022 20:17:14 -0400 Subject: [PATCH 145/496] Honor '/'-separated names in ResourceContainer.joinpath. --- importlib_resources/simple.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index 1f6e43a2..d0fbf237 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,13 +99,19 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - def joinpath(self, *names): - if not names: + @staticmethod + def _flatten(compound_names): + for name in compound_names: + yield from name.split('/') + + def joinpath(self, *descendants): + if not descendants: return self - name, rest = names[0], names[1:] + names = self._flatten(descendants) + target = next(names) return next( - traversable for traversable in self.iterdir() if traversable.name == name - ).joinpath(*rest) + traversable for traversable in self.iterdir() if traversable.name == target + ).joinpath(*names) class TraversableReader(TraversableResources, SimpleReader): From 07213e3acc47946c9377d9e8ba58fd752929d5de Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Apr 2022 20:42:39 -0400 Subject: [PATCH 146/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 290694e3..1055a2f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.7.1 +====== + +* #249: In ``simple.ResourceContainer.joinpath``, honor + names split by ``posixpath.sep``. + v5.7.0 ====== From 1a6b828304e7a8896b55d9ebf83f481ba7ebd568 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 22 Apr 2022 17:43:46 +0200 Subject: [PATCH 147/496] 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 fe3120aad4f9f68be7b1af4931fed350208701bd Mon Sep 17 00:00:00 2001 From: Saniya Maheshwari Date: Tue, 24 May 2022 23:34:04 +0530 Subject: [PATCH 148/496] Removed footnote stating that PEP 420 namespace packages are ignored --- docs/using.rst | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 548fa58e..6eac1a63 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -31,7 +31,7 @@ If you have a file system layout such as:: then the directories are ``data``, ``data/one``, and ``data/two``. Each of these are also Python packages by virtue of the fact that they all contain -``__init__.py`` files [#fn1]_. That means that in Python, all of these import +``__init__.py`` files. That means that in Python, all of these import statements work:: import data @@ -41,7 +41,7 @@ statements work:: Each import statement gives you a Python *module* corresponding to the ``__init__.py`` file in each of the respective directories. These modules are packages since packages are just special module instances that have an -additional attribute, namely a ``__path__`` [#fn2]_. +additional attribute, namely a ``__path__`` [#fn1]_. In this analogy then, resources are just files or directories contained in a package directory, so @@ -108,7 +108,7 @@ Packages or package names All of the ``importlib_resources`` APIs take a *package* as their first parameter, but this can either be a package name (as a ``str``) or an actual -module object, though the module *must* be a package [#fn3]_. If a string is +module object, though the module *must* be a package [#fn2]_. If a string is passed in, it must name an importable Python package, and this is first imported. Thus the above example could also be written as:: @@ -192,18 +192,12 @@ should return a ``TraversableResources`` instance. .. rubric:: Footnotes -.. [#fn1] We're ignoring `PEP 420 - `_ style namespace - packages, since ``importlib_resources`` does not support resources - within namespace packages. Also, the example assumes that the - parent directory containing ``data/`` is on ``sys.path``. - -.. [#fn2] As of `PEP 451 `_ this +.. [#fn1] As of `PEP 451 `_ this information is also available on the module's ``__spec__.submodule_search_locations`` attribute, which will not be ``None`` for packages. -.. [#fn3] Specifically, this means that in Python 2, the module object must +.. [#fn2] Specifically, this means that in Python 2, the module object must have an ``__path__`` attribute, while in Python 3, the module's ``__spec__.submodule_search_locations`` must not be ``None``. Otherwise a ``TypeError`` is raised. From ccb1161678d2173d94b92a8913b7c2fdb27cf537 Mon Sep 17 00:00:00 2001 From: Saniya Maheshwari Date: Thu, 26 May 2022 21:28:26 +0530 Subject: [PATCH 149/496] Removed Python 2 related footnote --- docs/using.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 6eac1a63..2b59b0cc 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -108,7 +108,7 @@ Packages or package names All of the ``importlib_resources`` APIs take a *package* as their first parameter, but this can either be a package name (as a ``str``) or an actual -module object, though the module *must* be a package [#fn2]_. If a string is +module object, though the module *must* be a package. If a string is passed in, it must name an importable Python package, and this is first imported. Thus the above example could also be written as:: @@ -197,12 +197,6 @@ should return a ``TraversableResources`` instance. ``__spec__.submodule_search_locations`` attribute, which will not be ``None`` for packages. -.. [#fn2] Specifically, this means that in Python 2, the module object must - have an ``__path__`` attribute, while in Python 3, the module's - ``__spec__.submodule_search_locations`` must not be ``None``. - Otherwise a ``TypeError`` is raised. - - .. _`pkg_resources API`: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access .. _`loader`: https://docs.python.org/3/reference/import.html#finders-and-loaders .. _`ResourceReader`: https://docs.python.org/3.7/library/importlib.html#importlib.abc.ResourceReader From 10bf1b1fb9e09e9836bea9e2edec620cd9eea7f9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jun 2022 21:37:40 -0400 Subject: [PATCH 150/496] 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 151/496] 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 152/496] 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 153/496] 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 154/496] 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 e39642364b5fff461b719e435d69374568ebcd3b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:41:10 -0400 Subject: [PATCH 155/496] Inline the flattening of descendants. --- importlib_resources/simple.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index d0fbf237..8c5d88cf 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,15 +99,14 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - @staticmethod - def _flatten(compound_names): - for name in compound_names: - yield from name.split('/') - def joinpath(self, *descendants): if not descendants: return self - names = self._flatten(descendants) + names = ( + name + for compound in descendants + for name in compound.split('/') + ) target = next(names) return next( traversable for traversable in self.iterdir() if traversable.name == target From df36958ed2ce587ad583f8d8d65b4670665e6050 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:46:38 -0400 Subject: [PATCH 156/496] Move ResourceContainer.joinpath to Traversable.joinpath, providing a concrete implementation. --- importlib_resources/abc.py | 8 +++++++- importlib_resources/simple.py | 13 ------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 5e38ba61..ac31fb96 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -92,7 +92,6 @@ def is_file(self) -> bool: Return True if self is a file """ - @abc.abstractmethod def joinpath(self, *descendants: StrPath) -> "Traversable": """ Return Traversable resolved with any descendants applied. @@ -101,6 +100,13 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": and each may contain multiple levels separated by ``posixpath.sep`` (``/``). """ + if not descendants: + return self + names = (name for compound in descendants for name in compound.split('/')) + target = next(names) + return next( + traversable for traversable in self.iterdir() if traversable.name == target + ).joinpath(*names) def __truediv__(self, child: StrPath) -> "Traversable": """ diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index 8c5d88cf..b85e4694 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -99,19 +99,6 @@ def iterdir(self): def open(self, *args, **kwargs): raise IsADirectoryError() - def joinpath(self, *descendants): - if not descendants: - return self - names = ( - name - for compound in descendants - for name in compound.split('/') - ) - target = next(names) - return next( - traversable for traversable in self.iterdir() if traversable.name == target - ).joinpath(*names) - class TraversableReader(TraversableResources, SimpleReader): """ From b923ce204247f6f0fd277243e08faf1238593bf9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 18:55:14 -0400 Subject: [PATCH 157/496] Refactor to converge descendants to a single type and rely on Path.parts for getting the parts. --- importlib_resources/abc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index ac31fb96..fc316a82 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,5 +1,7 @@ import abc import io +import itertools +import pathlib from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional from ._compat import runtime_checkable, Protocol, StrPath @@ -102,7 +104,9 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": """ if not descendants: return self - names = (name for compound in descendants for name in compound.split('/')) + names = itertools.chain.from_iterable( + path.parts for path in map(pathlib.PurePosixPath, descendants) + ) target = next(names) return next( traversable for traversable in self.iterdir() if traversable.name == target From c3cefa6100eeef0e40ca570d74585fe9d02d1e53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 19:19:25 -0400 Subject: [PATCH 158/496] Replace StopIteration with a TraversalError for use in capturing a failed traversal. --- importlib_resources/abc.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index fc316a82..7ab38b66 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -52,6 +52,10 @@ def contents(self) -> Iterable[str]: raise FileNotFoundError +class TraversalError(Exception): + pass + + @runtime_checkable class Traversable(Protocol): """ @@ -108,9 +112,15 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": path.parts for path in map(pathlib.PurePosixPath, descendants) ) target = next(names) - return next( + matches = ( traversable for traversable in self.iterdir() if traversable.name == target - ).joinpath(*names) + ) + try: + return next(matches).joinpath(*names) + except StopIteration: + raise TraversalError( + "Target not found during traversal.", target, list(names) + ) def __truediv__(self, child: StrPath) -> "Traversable": """ From 64403234fdbc3b9df60a57ff1ee76778a03a83b4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 19:22:25 -0400 Subject: [PATCH 159/496] In readers.MultiplexedPath, re-use Traversable.joinpath. --- importlib_resources/readers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index f1190ca4..c59e11ba 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -82,15 +82,16 @@ def is_dir(self): def is_file(self): return False - def joinpath(self, child): - # first try to find child in current paths - for file in self.iterdir(): - if file.name == child: - return file - # if it does not exist, construct it with the first path - return self._paths[0] / child - - __truediv__ = joinpath + def joinpath(self, *descendants): + try: + return super().joinpath(*descendants) + except abc.TraversalError as exc: + # One of the paths didn't resolve. + msg, target, names = exc.args + if names: + raise + # It was the last; construct result with the first path. + return self._paths[0].joinpath(target) def open(self, *args, **kwargs): raise FileNotFoundError(f'{self} is not a file') From 075b6ceab4b926e78721b614b34e4f1c8bb72758 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 20:12:53 -0400 Subject: [PATCH 160/496] Add test for empty joinpath. --- importlib_resources/tests/test_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 16841a50..37550cad 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -75,6 +75,7 @@ def test_join_path(self): str(path.joinpath('imaginary'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'imaginary'), ) + self.assertEqual(path.joinpath(), path) def test_repr(self): self.assertEqual( From 0140430e80c8adbdfb148510e219051fefa86730 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Apr 2022 20:17:01 -0400 Subject: [PATCH 161/496] Leave this behavior uncovered. --- importlib_resources/readers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index c59e11ba..19450f46 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -88,7 +88,7 @@ def joinpath(self, *descendants): except abc.TraversalError as exc: # One of the paths didn't resolve. msg, target, names = exc.args - if names: + if names: # pragma: nocover raise # It was the last; construct result with the first path. return self._paths[0].joinpath(target) From 643caba9f150fd1a9175560338f63d91dedfd454 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 20:36:10 -0400 Subject: [PATCH 162/496] Separate match resolution from recursive join. --- importlib_resources/abc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 7ab38b66..efa86ac6 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -116,11 +116,12 @@ def joinpath(self, *descendants: StrPath) -> "Traversable": traversable for traversable in self.iterdir() if traversable.name == target ) try: - return next(matches).joinpath(*names) + match = next(matches) except StopIteration: raise TraversalError( "Target not found during traversal.", target, list(names) ) + return match.joinpath(*names) def __truediv__(self, child: StrPath) -> "Traversable": """ From 1def9a2cba956d00fb20b3dd2feb4a8878047a21 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 21:01:28 -0400 Subject: [PATCH 163/496] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1055a2f0..5321b177 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.8.0 +====== + +* #250: Now ``Traversable.joinpath`` provides a concrete + implementation, replacing the implementation in ``.simple`` + and converging with the behavior in ``MultiplexedPath``. + v5.7.1 ====== From 467d4e5c27847f7fc68532711e21f669b6056baa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 21:05:33 -0400 Subject: [PATCH 164/496] Bump behaviors as found in CPython --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b7b3cc20..2bad3339 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,9 @@ were contributed to different versions in the standard library: * - importlib_resources - stdlib - * - 5.4 + * - 5.8 + - 3.12 + * - 5.7 - 3.11 * - 5.0 - 3.10 From 4025963e64e886baaa26c2f141d9da5bc81cc53a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 13 Jun 2022 19:24:00 +0200 Subject: [PATCH 165/496] gh-93353: Fix importlib.resources._tempfile() finalizer (#93377) Fix the importlib.resources.as_file() context manager to remove the temporary file if destroyed late during Python finalization: keep a local reference to the os.remove() function. Patch by Victor Stinner. --- importlib_resources/_common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index a12e2c75..775647cc 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -67,7 +67,10 @@ def from_package(package): @contextlib.contextmanager -def _tempfile(reader, suffix=''): +def _tempfile(reader, suffix='', + # gh-93353: Keep a reference to call os.remove() in late Python + # finalization. + *, _os_remove=os.remove): # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' # blocks due to the need to close the temporary file to work on Windows # properly. @@ -81,7 +84,7 @@ def _tempfile(reader, suffix=''): yield pathlib.Path(raw_path) finally: try: - os.remove(raw_path) + _os_remove(raw_path) except FileNotFoundError: pass From fb5ddebdede8128097cb00714b7e3ea455fc0143 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 21:13:11 -0400 Subject: [PATCH 166/496] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_resources/_common.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 775647cc..b2c1939c 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -67,10 +67,14 @@ def from_package(package): @contextlib.contextmanager -def _tempfile(reader, suffix='', - # gh-93353: Keep a reference to call os.remove() in late Python - # finalization. - *, _os_remove=os.remove): +def _tempfile( + reader, + suffix='', + # gh-93353: Keep a reference to call os.remove() in late Python + # finalization. + *, + _os_remove=os.remove, +): # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' # blocks due to the need to close the temporary file to work on Windows # properly. From f35390c3683abc8a41b243ca8ea98f48d3051e40 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 21:28:36 -0400 Subject: [PATCH 167/496] Use mod. Fixes doc build failures. --- docs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 798da5c8..dc1a3d29 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,7 @@ importable `Python package`_. Resources can live on the file system or in a zip file, with support for other loader_ classes that implement the appropriate API for reading resources. -``importlib_resources`` supplies a backport of -:doc:`importlib.resources `, +``importlib_resources`` supplies a backport of :mod:`importlib.resources`, enabling early access to features of future Python versions and making functionality available for older Python versions. Users are encouraged to use the Python standard library where suitable and fall back to From 3a11c0439239f0bf3e32371180f630e78253e7eb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 22:14:30 -0400 Subject: [PATCH 168/496] Update license. Fixes python/importlib_metadata#387. --- LICENSE | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 199 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index 378b991a..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,202 @@ -Copyright 2017-2019 Brett Cannon, Barry Warsaw -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 2678a7e82d581c07691575d90cd255b64ee63a27 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Jun 2022 15:56:54 -0400 Subject: [PATCH 169/496] 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 d63403be8fa7b7344e40e878858c40bab7414324 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 13:33:39 -0400 Subject: [PATCH 170/496] Add test capturing expectation revealed by #253. --- importlib_resources/tests/test_reader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 16841a50..0ea3a086 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -76,6 +76,10 @@ def test_join_path(self): os.path.join('namespacedata01', 'imaginary'), ) + def test_join_path_compound(self): + path = MultiplexedPath(self.folder) + assert not path.joinpath('imaginary/foo.py').exists() + def test_repr(self): self.assertEqual( repr(MultiplexedPath(self.folder)), From bad45d5b5c311598437cb720d58d2543d6dbff03 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 13:44:50 -0400 Subject: [PATCH 171/496] Suppress all TraversalErrors in MultiplexedPath. Fixes #253. --- importlib_resources/readers.py | 11 ++++------- importlib_resources/tests/test_reader.py | 3 --- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 19450f46..ab34db74 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -85,13 +85,10 @@ def is_file(self): def joinpath(self, *descendants): try: return super().joinpath(*descendants) - except abc.TraversalError as exc: - # One of the paths didn't resolve. - msg, target, names = exc.args - if names: # pragma: nocover - raise - # It was the last; construct result with the first path. - return self._paths[0].joinpath(target) + except abc.TraversalError: + # One of the paths did not resolve (a directory does not exist). + # Just return something that will not exist. + return self._paths[0].joinpath(*descendants) def open(self, *args, **kwargs): raise FileNotFoundError(f'{self} is not a file') diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 6148c31c..1c8ebeeb 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -3,8 +3,6 @@ import pathlib import unittest -import pytest - from importlib import import_module from importlib_resources.readers import MultiplexedPath, NamespaceReader @@ -79,7 +77,6 @@ def test_join_path(self): ) self.assertEqual(path.joinpath(), path) - @pytest.mark.xfail(reason="#253") def test_join_path_compound(self): path = MultiplexedPath(self.folder) assert not path.joinpath('imaginary/foo.py').exists() From 41d8e32e18998033ea9892412eb993458b78688b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 13:45:43 -0400 Subject: [PATCH 172/496] Update changelog. Ref #253. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5321b177..570fe86f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.8.1 +====== + +* #253: In ``MultiplexedPath``, restore expectation that + a compound path with a non-existent directory does not + raise an exception. + v5.8.0 ====== From a4e0cc72b6d0ed229b60bb455d7bc758b22132f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 14:37:12 -0400 Subject: [PATCH 173/496] Add _common._as_tree to demonstrate a directory of resources. --- importlib_resources/_common.py | 35 ++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index b2c1939c..beee62c0 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Optional +from typing import Union, Optional, ContextManager from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -94,7 +94,7 @@ def _tempfile( @functools.singledispatch -def as_file(path): +def as_file(path: Traversable) -> pathlib.Path: """ Given a Traversable object, return that object as a path on the local file system in a context manager. @@ -109,3 +109,34 @@ def _(path): Degenerate behavior for pathlib.Path objects. """ yield path + + +@contextlib.contextmanager +def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path]: + """ + Wrap tempfile.TemporyDirectory to return a pathlib object. + """ + with dir as result: + yield pathlib.Path(result) + + +@contextlib.contextmanager +def _as_tree(path: Traversable) -> pathlib.Path: + """ + Given a traversable dir, recursively replicate the whole tree + to the file system in a context manager. + """ + assert path.is_dir() + with _temp_path(tempfile.TemporaryDirectory(suffix=path.name)) as temp_dir: + _write_contents(temp_dir, path) + yield temp_dir + + +def _write_contents(target: pathlib.Path, source: Traversable): + for item in source.iterdir(): + child = target.joinpath(item.name) + if item.is_dir(): + child.mkdir() + _write_contents(child, item) + else: + child.open('wb').write(item.read_bytes()) From 1151390b5d57f1da6cf2a3ec42a9c5e1ec4c90bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:06:09 -0400 Subject: [PATCH 174/496] Remove type hints as they're causing trouble. --- importlib_resources/_common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index beee62c0..ac3e7cbe 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Optional, ContextManager +from typing import Union, Optional from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -94,7 +94,7 @@ def _tempfile( @functools.singledispatch -def as_file(path: Traversable) -> pathlib.Path: +def as_file(path): """ Given a Traversable object, return that object as a path on the local file system in a context manager. @@ -112,7 +112,7 @@ def _(path): @contextlib.contextmanager -def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path]: +def _temp_path(dir: tempfile.TemporaryDirectory): """ Wrap tempfile.TemporyDirectory to return a pathlib object. """ @@ -121,7 +121,7 @@ def _temp_path(dir: tempfile.TemporaryDirectory) -> ContextManager[pathlib.Path] @contextlib.contextmanager -def _as_tree(path: Traversable) -> pathlib.Path: +def _as_tree(path): """ Given a traversable dir, recursively replicate the whole tree to the file system in a context manager. @@ -132,7 +132,7 @@ def _as_tree(path: Traversable) -> pathlib.Path: yield temp_dir -def _write_contents(target: pathlib.Path, source: Traversable): +def _write_contents(target, source): for item in source.iterdir(): child = target.joinpath(item.name) if item.is_dir(): From 95c77d4150ecf4f630d890ed63f95f5168b0068e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:20:20 -0400 Subject: [PATCH 175/496] Allow as_file to return a context manager for a directory or a file as needed. --- importlib_resources/_common.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index ac3e7cbe..8735109d 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -93,13 +93,22 @@ def _tempfile( pass +def _temp_file(path): + return _tempfile(path.read_bytes, suffix=path.name) + + @functools.singledispatch def as_file(path): """ Given a Traversable object, return that object as a path on the local file system in a context manager. """ - return _tempfile(path.read_bytes, suffix=path.name) + try: + is_dir = path.is_dir() + except FileNotFoundError: + is_dir = False + + return _temp_dir(path) if is_dir else _temp_file(path) @as_file.register(pathlib.Path) @@ -121,7 +130,7 @@ def _temp_path(dir: tempfile.TemporaryDirectory): @contextlib.contextmanager -def _as_tree(path): +def _temp_dir(path): """ Given a traversable dir, recursively replicate the whole tree to the file system in a context manager. From 9e17e0d1c7adc76b0fe9a722b1102d4279f22429 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 15:29:05 -0400 Subject: [PATCH 176/496] Rewrite _temp_dir so that it produces a directory of the same name as the input path. --- importlib_resources/_common.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 8735109d..5a9f3c86 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -136,16 +136,16 @@ def _temp_dir(path): to the file system in a context manager. """ assert path.is_dir() - with _temp_path(tempfile.TemporaryDirectory(suffix=path.name)) as temp_dir: - _write_contents(temp_dir, path) - yield temp_dir + with _temp_path(tempfile.TemporaryDirectory()) as temp_dir: + yield _write_contents(temp_dir, path) def _write_contents(target, source): - for item in source.iterdir(): - child = target.joinpath(item.name) - if item.is_dir(): - child.mkdir() + child = target.joinpath(source.name) + if source.is_dir(): + child.mkdir() + for item in source.iterdir(): _write_contents(child, item) - else: - child.open('wb').write(item.read_bytes()) + else: + child.open('wb').write(source.read_bytes()) + return child From f15de8cbe24887186d51dbed70634ea055df8441 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 19:04:05 -0400 Subject: [PATCH 177/496] Add test capturing new expectation that subdirectories are supported. --- importlib_resources/tests/test_resource.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 5affd8b0..82390271 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -111,6 +111,14 @@ def test_submodule_contents_by_name(self): {'__init__.py', 'binary.file'}, ) + def test_as_file_directory(self): + with resources.as_file(resources.files('ziptestdata')) as data: + assert data.name == 'ziptestdata' + assert data.is_dir() + assert data.joinpath('subdirectory').is_dir() + assert len(list(data.iterdir())) + assert not data.parent.exists() + class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): ZIP_MODULE = zipdata02 # type: ignore From 92666d25b5561f4033304efc35c5819f81a52a94 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Jul 2022 19:10:28 -0400 Subject: [PATCH 178/496] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5321b177..f6817bc7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.9.0 +====== + +* #228: ``as_file`` now also supports a ``Traversable`` + representing a directory and (when needed) renders the + full tree to a temporary directory. + v5.8.0 ====== From ccbb0d396b0057ee70a3e8f36b7e8418c211c826 Mon Sep 17 00:00:00 2001 From: chanicpanic <51764816+chanicpanic@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:55:15 -0700 Subject: [PATCH 179/496] Docs: Change isdir() to is_dir() For Path objects, the function name contains an underscore. --- docs/migration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration.rst b/docs/migration.rst index 6828f0b8..fa2155de 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -148,7 +148,7 @@ a package is a directory or not:: The ``importlib_resources`` equivalent is straightforward:: - if importlib_resources.files('my.package').joinpath('resource').isdir(): + if importlib_resources.files('my.package').joinpath('resource').is_dir(): print('A directory') From fea1e7cdd57d330f22ac54512ae2df19083c6ec7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jul 2022 18:53:07 -0400 Subject: [PATCH 180/496] 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 bac6e8e3e6c681d923e1ec78b6f670cee0883ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Jul 2022 12:29:00 -0400 Subject: [PATCH 181/496] Extract function for _is_present_dir. --- importlib_resources/_common.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 5a9f3c86..423b0bb9 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -97,18 +97,26 @@ def _temp_file(path): return _tempfile(path.read_bytes, suffix=path.name) +def _is_present_dir(path: Traversable) -> bool: + """ + Some Traversables implement ``is_dir()`` to raise an + exception (i.e. ``FileNotFoundError``) when the + directory doesn't exist. This function wraps that call + to always return a boolean and only return True + if there's a dir and it exists. + """ + with contextlib.suppress(FileNotFoundError): + return path.is_dir() + return False + + @functools.singledispatch def as_file(path): """ Given a Traversable object, return that object as a path on the local file system in a context manager. """ - try: - is_dir = path.is_dir() - except FileNotFoundError: - is_dir = False - - return _temp_dir(path) if is_dir else _temp_file(path) + return _temp_dir(path) if _is_present_dir(path) else _temp_file(path) @as_file.register(pathlib.Path) From de976a0d56a1f3e9ff0418b213be0421378732a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 Jul 2022 12:30:00 -0400 Subject: [PATCH 182/496] Inline type defintions --- importlib_resources/_common.py | 12 +++---- importlib_resources/_legacy.py | 3 +- importlib_resources/simple.py | 62 +++++++++++++++------------------- 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 423b0bb9..3014ae10 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -14,16 +14,14 @@ Package = Union[types.ModuleType, str] -def files(package): - # type: (Package) -> Traversable +def files(package: Package) -> Traversable: """ Get a Traversable resource from a package """ return from_package(get_package(package)) -def get_resource_reader(package): - # type: (types.ModuleType) -> Optional[ResourceReader] +def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: """ Return the package's loader if it's a ResourceReader. """ @@ -39,13 +37,11 @@ def get_resource_reader(package): return reader(spec.name) # type: ignore -def resolve(cand): - # type: (Package) -> types.ModuleType +def resolve(cand: Package) -> types.ModuleType: return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) -def get_package(package): - # type: (Package) -> types.ModuleType +def get_package(package: Package) -> types.ModuleType: """Take a package name or module object and return the module. Raise an exception if the resolved module is not a package. diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py index 1d5d3f1f..b1ea8105 100644 --- a/importlib_resources/_legacy.py +++ b/importlib_resources/_legacy.py @@ -27,8 +27,7 @@ def wrapper(*args, **kwargs): return wrapper -def normalize_path(path): - # type: (Any) -> str +def normalize_path(path: Any) -> str: """Normalize a path by ensuring it is a string. If the resulting string contains path separators, an exception is raised. diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index b85e4694..c23ade1b 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -17,30 +17,26 @@ class SimpleReader(abc.ABC): """ @abc.abstractproperty - def package(self): - # type: () -> str + def package(self) -> str: """ The name of the package for which this reader loads resources. """ @abc.abstractmethod - def children(self): - # type: () -> List['SimpleReader'] + def children(self) -> List['SimpleReader']: """ Obtain an iterable of SimpleReader for available child containers (e.g. directories). """ @abc.abstractmethod - def resources(self): - # type: () -> List[str] + def resources(self) -> List[str]: """ Obtain available named resources for this virtual package. """ @abc.abstractmethod - def open_binary(self, resource): - # type: (str) -> BinaryIO + def open_binary(self, resource: str) -> BinaryIO: """ Obtain a File-like for a named resource. """ @@ -50,13 +46,35 @@ def name(self): return self.package.split('.')[-1] +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader: SimpleReader): + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + class ResourceHandle(Traversable): """ Handle to a named resource in a ResourceReader. """ - def __init__(self, parent, name): - # type: (ResourceContainer, str) -> None + def __init__(self, parent: ResourceContainer, name: str): self.parent = parent self.name = name # type: ignore @@ -76,30 +94,6 @@ def joinpath(self, name): raise RuntimeError("Cannot traverse into a resource") -class ResourceContainer(Traversable): - """ - Traversable container for a package's resources via its reader. - """ - - def __init__(self, reader): - # type: (SimpleReader) -> None - self.reader = reader - - def is_dir(self): - return True - - def is_file(self): - return False - - def iterdir(self): - files = (ResourceHandle(self, name) for name in self.reader.resources) - dirs = map(ResourceContainer, self.reader.children()) - return itertools.chain(files, dirs) - - def open(self, *args, **kwargs): - raise IsADirectoryError() - - class TraversableReader(TraversableResources, SimpleReader): """ A TraversableResources based on SimpleReader. Resource providers From 56c21cd058dcc200c38cccd81fde81ad06d934db Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 Jul 2022 12:36:19 -0400 Subject: [PATCH 183/496] Implement resolve as a single-dispatch function. (#261) --- importlib_resources/_common.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 3014ae10..b8e2e1f0 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -37,8 +37,14 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: return reader(spec.name) # type: ignore -def resolve(cand: Package) -> types.ModuleType: - return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) +@functools.singledispatch +def resolve(cand: Package): + return cand + + +@resolve.register +def _(cand: str): + return importlib.import_module(cand) def get_package(package: Package) -> types.ModuleType: From 325916c8240b8b3c7c41f24b664ca591e8555ea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 10:12:46 -0400 Subject: [PATCH 184/496] 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 185/496] 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 186/496] 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 187/496] 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 188/496] 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 57a5e3ea0a8ca7b0fc21625cd1b0ff6b2f7e2370 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 13 Sep 2022 22:48:55 +0300 Subject: [PATCH 189/496] Replace `abstractproperty` with `property` + `abstractmethod` --- importlib_resources/abc.py | 3 ++- importlib_resources/simple.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index efa86ac6..23b6aeaf 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -139,7 +139,8 @@ def open(self, mode='r', *args, **kwargs): accepted by io.TextIOWrapper. """ - @abc.abstractproperty + @property + @abc.abstractmethod def name(self) -> str: """ The base name of this object without any parent references. diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index c23ade1b..7770c922 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -16,7 +16,8 @@ class SimpleReader(abc.ABC): provider. """ - @abc.abstractproperty + @property + @abc.abstractmethod def package(self) -> str: """ The name of the package for which this reader loads resources. From 27c55340e745741773e875402d20ecbb7fade521 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:31:09 -0400 Subject: [PATCH 190/496] 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 b4f0ae621594316e56cede2856b6a5be605a893c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:46:38 -0400 Subject: [PATCH 191/496] 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 679eebb215c80c7376a1df02c77fd368347620b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 12:41:25 -0400 Subject: [PATCH 192/496] 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 193/496] 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 94d59b6c5d3ea0290116aae4bcd073e388edc03c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Jul 2022 13:38:00 -0400 Subject: [PATCH 194/496] Add test capturing current behavior and missed expectation. Ref #203. --- .coveragerc | 1 + importlib_resources/tests/_compat.py | 15 +++++++- importlib_resources/tests/_path.py | 50 +++++++++++++++++++++++++ importlib_resources/tests/test_files.py | 28 ++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 importlib_resources/tests/_path.py diff --git a/.coveragerc b/.coveragerc index 2913b113..d7b2fedb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ omit = */_itertools.py */_legacy.py */simple.py + */_path.py [report] show_missing = True diff --git a/importlib_resources/tests/_compat.py b/importlib_resources/tests/_compat.py index 4c99cffd..e7bf06dd 100644 --- a/importlib_resources/tests/_compat.py +++ b/importlib_resources/tests/_compat.py @@ -6,7 +6,20 @@ except ImportError: # Python 3.9 and earlier class import_helper: # type: ignore - from test.support import modules_setup, modules_cleanup + from test.support import ( + modules_setup, + modules_cleanup, + DirsOnSysPath, + CleanImport, + ) + + +try: + from test.support import os_helper # type: ignore +except ImportError: + # Python 3.9 compat + class os_helper: # type:ignore + from test.support import temp_dir try: diff --git a/importlib_resources/tests/_path.py b/importlib_resources/tests/_path.py new file mode 100644 index 00000000..c630e4d3 --- /dev/null +++ b/importlib_resources/tests/_path.py @@ -0,0 +1,50 @@ +import pathlib +import functools + + +#### +# from jaraco.path 3.4 + + +def build(spec, prefix=pathlib.Path()): + """ + Build a set of files/directories, as described by the spec. + + Each key represents a pathname, and the value represents + the content. Content may be a nested directory. + + >>> spec = { + ... 'README.txt': "A README file", + ... "foo": { + ... "__init__.py": "", + ... "bar": { + ... "__init__.py": "", + ... }, + ... "baz.py": "# Some code", + ... } + ... } + >>> tmpdir = getfixture('tmpdir') + >>> build(spec, tmpdir) + """ + for name, contents in spec.items(): + create(contents, pathlib.Path(prefix) / name) + + +@functools.singledispatch +def create(content, path): + path.mkdir(exist_ok=True) + build(content, prefix=path) # type: ignore + + +@create.register +def _(content: bytes, path): + path.write_bytes(content) + + +@create.register +def _(content: str, path): + path.write_text(content) + + +# end from jaraco.path +#### diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 2676b49e..b1d94d02 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,10 +1,13 @@ import typing import unittest +import contextlib import importlib_resources as resources from importlib_resources.abc import Traversable from . import data01 from . import util +from . import _path +from ._compat import os_helper, import_helper class FilesTests: @@ -42,5 +45,30 @@ def setUp(self): self.data = namespacedata01 +class ModulesFilesTests(unittest.TestCase): + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) + self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) + self.fixtures.enter_context(import_helper.CleanImport()) + + def test_module_resources(self): + """ + A module can have resources found adjacent to the module. + """ + spec = { + 'mod.py': '', + 'res.txt': 'resources are the best', + } + _path.build(spec, self.site_dir) + import mod + + # currently a failure occurs; ref #203 + with self.assertRaisesRegex(TypeError, '.*mod.* is not a package'): + actual = resources.files(mod).joinpath('res.txt').read_text() + assert actual == spec['res.txt'] + + if __name__ == '__main__': unittest.main() From 5b1281d8abac08db0526b6f4876e8c67c97b774e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Jul 2022 14:02:51 -0400 Subject: [PATCH 195/496] Remove restriction that a 'package' cannot be a module. Allows resolution of resources from adjacent modules, even those not found in a package. Fixes #203. --- importlib_resources/_common.py | 13 +------------ importlib_resources/tests/test_files.py | 6 ++---- importlib_resources/tests/util.py | 11 ----------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index b8e2e1f0..07aab63b 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -18,7 +18,7 @@ def files(package: Package) -> Traversable: """ Get a Traversable resource from a package """ - return from_package(get_package(package)) + return from_package(resolve(package)) def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: @@ -47,17 +47,6 @@ def _(cand: str): return importlib.import_module(cand) -def get_package(package: Package) -> types.ModuleType: - """Take a package name or module object and return the module. - - Raise an exception if the resolved module is not a package. - """ - resolved = resolve(package) - if wrap_spec(resolved).submodule_search_locations is None: - raise TypeError(f'{package!r} is not a package') - return resolved - - def from_package(package): """ Return a Traversable object for the given package. diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index b1d94d02..d88e7e9f 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -64,10 +64,8 @@ def test_module_resources(self): _path.build(spec, self.site_dir) import mod - # currently a failure occurs; ref #203 - with self.assertRaisesRegex(TypeError, '.*mod.* is not a package'): - actual = resources.files(mod).joinpath('res.txt').read_text() - assert actual == spec['res.txt'] + actual = resources.files(mod).joinpath('res.txt').read_text() + assert actual == spec['res.txt'] if __name__ == '__main__': diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index c6d83e4b..3c36a9d2 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -102,17 +102,6 @@ def test_importing_module_as_side_effect(self): del sys.modules[data01.__name__] self.execute(data01.__name__, 'utf-8.file') - def test_non_package_by_name(self): - # The anchor package cannot be a module. - with self.assertRaises(TypeError): - self.execute(__name__, 'utf-8.file') - - def test_non_package_by_package(self): - # The anchor package cannot be a module. - with self.assertRaises(TypeError): - module = sys.modules['importlib_resources.tests.util'] - self.execute(module, 'utf-8.file') - def test_missing_path(self): # Attempting to open or read or request the path for a # non-existent path should succeed if open_resource From a60496b3693fdc3c9e2106635239b27602dae5e5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Jul 2022 14:19:32 -0400 Subject: [PATCH 196/496] Update changelog. Ref #203. --- CHANGES.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 01e3a69f..da2ac6c5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v5.10.0 +======= + +* #203: Lifted restriction on modules passed to ``as_file``. + Now modules need not be a package and if a non-package + module is passed, resources will be resolved adjacent to + those modules, even for modules not found in any package. + For example, ``files(import_module('mod.py'))`` will + resolve resources found at the root. + v5.9.0 ====== From 8c3edebebbd477987c713cfbb7135928fc8420cc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 10:25:13 -0400 Subject: [PATCH 197/496] Update docs to prefer the name 'anchor' to 'package', as the anchor may be a module. --- docs/index.rst | 6 ++--- docs/using.rst | 40 +++++++++++++++++++--------------- importlib_resources/_common.py | 5 +++-- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 7482611e..260696f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,8 +6,8 @@ in Python packages. It provides functionality similar to ``pkg_resources`` `Basic Resource Access`_ API, but without all of the overhead and performance problems of ``pkg_resources``. -In our terminology, a *resource* is a file tree that is located within an -importable `Python package`_. Resources can live on the file system or in a +In our terminology, a *resource* is a file tree that is located alongside an +importable `Python module`_. Resources can live on the file system or in a zip file, with support for other loader_ classes that implement the appropriate API for reading resources. @@ -43,5 +43,5 @@ Indices and tables .. _`Basic Resource Access`: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access -.. _`Python package`: https://docs.python.org/3/reference/import.html#packages +.. _`Python module`: https://docs.python.org/3/glossary.html#term-module .. _loader: https://docs.python.org/3/reference/import.html#finders-and-loaders diff --git a/docs/using.rst b/docs/using.rst index 2b59b0cc..3403a6d9 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,11 +5,11 @@ =========================== ``importlib_resources`` is a library that leverages Python's import system to -provide access to *resources* within *packages*. Given that this library is -built on top of the import system, it is highly efficient and easy to use. -This library's philosophy is that, if you can import a package, you can access -resources within that package. Resources can be opened or read, in either -binary or text mode. +provide access to *resources* within *packages* and alongside *modules*. Given +that this library is built on top of the import system, it is highly efficient +and easy to use. This library's philosophy is that, if one can import a +module, one can access resources associated with that module. Resources can be +opened or read, in either binary or text mode. What exactly do we mean by "a resource"? It's easiest to think about the metaphor of files and directories on the file system, though it's important to @@ -23,11 +23,14 @@ If you have a file system layout such as:: one/ __init__.py resource1.txt + module1.py resources1/ resource1.1.txt two/ __init__.py resource2.txt + standalone.py + resource3.txt then the directories are ``data``, ``data/one``, and ``data/two``. Each of these are also Python packages by virtue of the fact that they all contain @@ -48,11 +51,14 @@ package directory, so ``data/one/resource1.txt`` and ``data/two/resource2.txt`` are both resources, as are the ``__init__.py`` files in all the directories. -Resources are always accessed relative to the package that they live in. -``resource1.txt`` and ``resources1/resource1.1.txt`` are resources within -the ``data.one`` package, and -``two/resource2.txt`` is a resource within the -``data`` package. +Resources in packages are always accessed relative to the package that they +live in. ``resource1.txt`` and ``resources1/resource1.1.txt`` are resources +within the ``data.one`` package, and ``two/resource2.txt`` is a resource +within the ``data`` package. + +Resources may also be referenced relative to another *anchor*, a module in a +package (``data.one.module1``) or a standalone module (``standalone``). In +this case, resources are loaded from the same loader that loaded that module. Example @@ -103,14 +109,14 @@ using ``importlib_resources`` would look like:: eml = files('email.tests.data').joinpath('message.eml').read_text() -Packages or package names -========================= +Anchors +======= -All of the ``importlib_resources`` APIs take a *package* as their first -parameter, but this can either be a package name (as a ``str``) or an actual -module object, though the module *must* be a package. If a string is -passed in, it must name an importable Python package, and this is first -imported. Thus the above example could also be written as:: +The ``importlib_resources`` ``files`` API takes an *anchor* as its first +parameter, which can either be a package name (as a ``str``) or an actual +module object. If a string is passed in, it must name an importable Python +module, which is imported prior to loading any resources. Thus the above +example could also be written as:: import email.tests.data eml = files(email.tests.data).joinpath('message.eml').read_text() diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 07aab63b..b2db5abf 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -12,11 +12,12 @@ from ._compat import wrap_spec Package = Union[types.ModuleType, str] +Anchor = Package -def files(package: Package) -> Traversable: +def files(package: Anchor) -> Traversable: """ - Get a Traversable resource from a package + Get a Traversable resource for an anchor. """ return from_package(resolve(package)) From 3300fc212f45a860181ba91d932d19e11642946a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 10:34:28 -0400 Subject: [PATCH 198/496] Rewrite section using imperative voice and add determiners. --- docs/using.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 2b59b0cc..7e7f1233 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -119,18 +119,18 @@ imported. Thus the above example could also be written as:: File system or zip file ======================= -In general you never have to worry whether your package is on the file system -or in a zip file, as the ``importlib_resources`` APIs hide those details from -you. Sometimes though, you need a path to an actual file on the file system. +A consumer need not worry whether any given package is on the file system +or in a zip file, as the ``importlib_resources`` APIs abstracts those details. +Sometimes though, the user needs a path to an actual file on the file system. For example, some SSL APIs require a certificate file to be specified by a real file system path, and C's ``dlopen()`` function also requires a real file system path. -To support this, ``importlib_resources`` provides an API that will extract the -resource from a zip file to a temporary file, and return the file system path -to this temporary file as a :py:class:`pathlib.Path` object. In order to -properly clean up this temporary file, what's actually returned is a context -manager that you can use in a ``with``-statement:: +To support this need, ``importlib_resources`` provides an API to extract the +resource from a zip file to a temporary file or folder and return the file +system path to this materialized resource as a :py:class:`pathlib.Path` +object. In order to properly clean up this temporary file, what's actually +returned is a context manager for use in a ``with``-statement:: from importlib_resources import files, as_file @@ -138,8 +138,7 @@ manager that you can use in a ``with``-statement:: with as_file(source) as eml: third_party_api_requiring_file_system_path(eml) -You can use all the standard :py:mod:`contextlib` APIs to manage this context -manager. +Use all the standard :py:mod:`contextlib` APIs to manage this context manager. .. attention:: From 4e4aef2de30f91882f1510f4027b6354464d5dff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 10:35:12 -0400 Subject: [PATCH 199/496] Remove docs pertinent to Python 3.6 and earlier. --- docs/using.rst | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 7e7f1233..ab0a1374 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -140,27 +140,6 @@ returned is a context manager for use in a ``with``-statement:: Use all the standard :py:mod:`contextlib` APIs to manage this context manager. -.. attention:: - - There is an odd interaction with Python 3.4, 3.5, and 3.6 regarding adding - zip or wheel file paths to ``sys.path``. Due to limitations in `zipimport - `_, which can't be - changed without breaking backward compatibility, you **must** use an - absolute path to the zip/wheel file. If you use a relative path, you will - not be able to find resources inside these zip files. E.g.: - - **No**:: - - sys.path.append('relative/path/to/foo.whl') - files('foo') # This will fail! - - **Yes**:: - - sys.path.append(os.path.abspath('relative/path/to/foo.whl')) - files('foo') - -Both relative and absolute paths work for Python 3.7 and newer. - Migrating from Legacy ===================== From 091f22946570abaad6aabceb5649adc64032cff0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 11:45:05 -0400 Subject: [PATCH 200/496] Extend type spec for clarity. --- importlib_resources/_common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index b2db5abf..1dcced6d 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -6,7 +6,7 @@ import types import importlib -from typing import Union, Optional +from typing import Union, Optional, cast from .abc import ResourceReader, Traversable from ._compat import wrap_spec @@ -39,16 +39,16 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: @functools.singledispatch -def resolve(cand: Package): - return cand +def resolve(cand: Anchor) -> types.ModuleType: + return cast(types.ModuleType, cand) @resolve.register -def _(cand: str): +def _(cand: str) -> types.ModuleType: return importlib.import_module(cand) -def from_package(package): +def from_package(package: types.ModuleType): """ Return a Traversable object for the given package. From 887ad2224009ec38b939688e990c5000c5d8dd8d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 15:53:26 -0400 Subject: [PATCH 201/496] Use more namespaces. --- importlib_resources/tests/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index c6d83e4b..7c990ab7 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -3,7 +3,7 @@ import io import sys import types -from pathlib import Path, PurePath +import pathlib from . import data01 from . import zipdata01 @@ -94,7 +94,7 @@ def test_string_path(self): def test_pathlib_path(self): # Passing in a pathlib.PurePath object for the path should succeed. - path = PurePath('utf-8.file') + path = pathlib.PurePath('utf-8.file') self.execute(data01, path) def test_importing_module_as_side_effect(self): @@ -144,7 +144,7 @@ class ZipSetupBase: @classmethod def setUpClass(cls): - data_path = Path(cls.ZIP_MODULE.__file__) + data_path = pathlib.Path(cls.ZIP_MODULE.__file__) data_dir = data_path.parent cls._zip_path = str(data_dir / 'ziptestdata.zip') sys.path.append(cls._zip_path) From be6d5e6fe0581e16ba382198eb904fe6120329b9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 15:56:23 -0400 Subject: [PATCH 202/496] Prefer relative imports for abc. --- importlib_resources/__init__.py | 2 +- importlib_resources/tests/test_files.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 15f6b26b..34e3a995 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -17,7 +17,7 @@ Resource, ) -from importlib_resources.abc import ResourceReader +from .abc import ResourceReader __all__ = [ diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 2676b49e..91a41085 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -2,7 +2,7 @@ import unittest import importlib_resources as resources -from importlib_resources.abc import Traversable +from ..abc import Traversable from . import data01 from . import util From 4c672fb9bec443a45bf084ec56233b8b418befee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 22:47:43 -0400 Subject: [PATCH 203/496] Correct the changelog to mention the correct function affected. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index da2ac6c5..edc31ac2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ v5.10.0 ======= -* #203: Lifted restriction on modules passed to ``as_file``. +* #203: Lifted restriction on modules passed to ``files``. Now modules need not be a package and if a non-package module is passed, resources will be resolved adjacent to those modules, even for modules not found in any package. From eb7f123376a5c7c5a90e83d25f949119d6d40e97 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 23:44:41 -0400 Subject: [PATCH 204/496] Rename the 'package' parameter to 'anchor'. --- CHANGES.rst | 4 +++- importlib_resources/_common.py | 30 +++++++++++++++++++++++-- importlib_resources/tests/test_files.py | 16 +++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index edc31ac2..6e3266ef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ v5.10.0 module is passed, resources will be resolved adjacent to those modules, even for modules not found in any package. For example, ``files(import_module('mod.py'))`` will - resolve resources found at the root. + resolve resources found at the root. The parameter to + files was renamed from 'package' to 'anchor', with a + compatibility shim for those passing by keyword. v5.9.0 ====== diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 1dcced6d..54668388 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -5,6 +5,7 @@ import contextlib import types import importlib +import warnings from typing import Union, Optional, cast from .abc import ResourceReader, Traversable @@ -15,11 +16,36 @@ Anchor = Package -def files(package: Anchor) -> Traversable: +def package_to_anchor(func): + """ + Replace 'package' parameter as 'anchor' and warn about the change. + """ + undefined = object() + + @functools.wraps(func) + def wrapper(anchor=undefined, package=undefined): + if package is not undefined: + if anchor is not undefined: + return func(anchor, package) + warnings.warn( + "First parameter to files is renamed to 'anchor'", + DeprecationWarning, + stacklevel=2, + ) + return func(package) + elif anchor is undefined: + return func() + return func(anchor) + + return wrapper + + +@package_to_anchor +def files(anchor: Anchor) -> Traversable: """ Get a Traversable resource for an anchor. """ - return from_package(resolve(package)) + return from_package(resolve(anchor)) def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index d88e7e9f..196469a3 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,5 +1,6 @@ import typing import unittest +import warnings import contextlib import importlib_resources as resources @@ -10,6 +11,13 @@ from ._compat import os_helper, import_helper +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + class FilesTests: def test_read_bytes(self): files = resources.files(self.data) @@ -28,6 +36,14 @@ def test_read_text(self): def test_traversable(self): assert isinstance(resources.files(self.data), Traversable) + def test_old_parameter(self): + """ + Files used to take a 'package' parameter. Make sure anyone + passing by name is still supported. + """ + with suppress_known_deprecation(): + resources.files(package=self.data) + class OpenDiskTests(FilesTests, unittest.TestCase): def setUp(self): From 4e63613fd98f2e5034c613442b4ea2b908cc03cc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Oct 2022 13:25:07 -0400 Subject: [PATCH 205/496] Add doctests to flesh out coverage. --- importlib_resources/_common.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 54668388..52af4a13 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -19,6 +19,15 @@ def package_to_anchor(func): """ Replace 'package' parameter as 'anchor' and warn about the change. + + Other errors should fall through. + + >>> files() + Traceback (most recent call last): + TypeError: files() missing 1 required positional argument: 'anchor' + >>> files('a', 'b') + Traceback (most recent call last): + TypeError: files() takes 1 positional argument but 2 were given """ undefined = object() From 8bb0635f79f613f9be9d30118939e4a81e84e8d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 22:49:50 -0400 Subject: [PATCH 206/496] Add test capturing expectation. --- importlib_resources/tests/test_files.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index dac08024..9481ec77 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,6 +1,8 @@ import typing +import textwrap import unittest import warnings +import importlib import contextlib import importlib_resources as resources @@ -84,5 +86,33 @@ def test_module_resources(self): assert actual == spec['res.txt'] +class ImplicitContextFilesTests(unittest.TestCase): + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) + self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) + self.fixtures.enter_context(import_helper.CleanImport()) + + @__import__('pytest').mark.xfail(reason="work in progress") + def test_implicit_files(self): + """ + Without any parameter, files() will infer the location as the caller. + """ + spec = { + 'somepkg': { + '__init__.py': textwrap.dedent( + """ + import importlib_resources as res + val = res.files().joinpath('res.txt').read_text() + """ + ), + 'res.txt': 'resources are the best', + }, + } + _path.build(spec, self.site_dir) + assert importlib.import_module('somepkg').val == 'resources are the best' + + if __name__ == '__main__': unittest.main() From 38f789d46073c4d03595ae207196dff728c38eef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 22:51:54 -0400 Subject: [PATCH 207/496] Extract fixture for SiteDir. --- importlib_resources/tests/test_files.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 9481ec77..c6c4eaaa 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -63,7 +63,7 @@ def setUp(self): self.data = namespacedata01 -class ModulesFilesTests(unittest.TestCase): +class SiteDir: def setUp(self): self.fixtures = contextlib.ExitStack() self.addCleanup(self.fixtures.close) @@ -71,6 +71,8 @@ def setUp(self): self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) self.fixtures.enter_context(import_helper.CleanImport()) + +class ModulesFilesTests(SiteDir, unittest.TestCase): def test_module_resources(self): """ A module can have resources found adjacent to the module. @@ -86,14 +88,7 @@ def test_module_resources(self): assert actual == spec['res.txt'] -class ImplicitContextFilesTests(unittest.TestCase): - def setUp(self): - self.fixtures = contextlib.ExitStack() - self.addCleanup(self.fixtures.close) - self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) - self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) - self.fixtures.enter_context(import_helper.CleanImport()) - +class ImplicitContextFilesTests(SiteDir, unittest.TestCase): @__import__('pytest').mark.xfail(reason="work in progress") def test_implicit_files(self): """ From e15a6b79ba1b11e21476f7274e0f2fafd5054486 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 11:45:05 -0400 Subject: [PATCH 208/496] Implement caller inference to allow ``files()`` to be called without any parameter. --- importlib_resources/_common.py | 33 ++++++++++++++++++++----- importlib_resources/tests/test_files.py | 1 - 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 52af4a13..9f19784d 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -5,7 +5,9 @@ import contextlib import types import importlib +import inspect import warnings +import itertools from typing import Union, Optional, cast from .abc import ResourceReader, Traversable @@ -22,12 +24,9 @@ def package_to_anchor(func): Other errors should fall through. - >>> files() - Traceback (most recent call last): - TypeError: files() missing 1 required positional argument: 'anchor' >>> files('a', 'b') Traceback (most recent call last): - TypeError: files() takes 1 positional argument but 2 were given + TypeError: files() takes from 0 to 1 positional arguments but 2 were given """ undefined = object() @@ -50,7 +49,7 @@ def wrapper(anchor=undefined, package=undefined): @package_to_anchor -def files(anchor: Anchor) -> Traversable: +def files(anchor: Optional[Anchor] = None) -> Traversable: """ Get a Traversable resource for an anchor. """ @@ -74,7 +73,7 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: @functools.singledispatch -def resolve(cand: Anchor) -> types.ModuleType: +def resolve(cand: Optional[Anchor]) -> types.ModuleType: return cast(types.ModuleType, cand) @@ -83,6 +82,28 @@ def _(cand: str) -> types.ModuleType: return importlib.import_module(cand) +@resolve.register +def _(cand: None) -> types.ModuleType: + return resolve(_infer_caller().f_globals['__name__']) + + +def _infer_caller(): + """ + Walk the stack and find the frame of the first caller not in this module. + """ + + def is_this_file(frame_info): + return frame_info.filename == __file__ + + def is_wrapper(frame_info): + return frame_info.function == 'wrapper' + + not_this_file = itertools.filterfalse(is_this_file, inspect.stack()) + # also exclude 'wrapper' due to singledispatch in the call stack + callers = itertools.filterfalse(is_wrapper, not_this_file) + return next(callers).frame + + def from_package(package: types.ModuleType): """ Return a Traversable object for the given package. diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index c6c4eaaa..d258fb5f 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -89,7 +89,6 @@ def test_module_resources(self): class ImplicitContextFilesTests(SiteDir, unittest.TestCase): - @__import__('pytest').mark.xfail(reason="work in progress") def test_implicit_files(self): """ Without any parameter, files() will infer the location as the caller. From d34eeaca3ab5721790ca2246c0153822ec687187 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Oct 2022 20:30:17 -0400 Subject: [PATCH 209/496] Update changelog --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6e3266ef..7e0225fe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,10 @@ v5.10.0 files was renamed from 'package' to 'anchor', with a compatibility shim for those passing by keyword. +* #259: ``files`` no longer requires the anchor to be + specified and can infer the anchor from the caller's scope + (defaults to the caller's module). + v5.9.0 ====== From e95c54fe607aaa980a064b6490312483381ba0ab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Oct 2022 11:35:13 -0400 Subject: [PATCH 210/496] 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 211/496] 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 6d64beb916bf6572f39973ea32b64268b2a4274a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Oct 2022 16:28:48 -0400 Subject: [PATCH 212/496] Bump importlib_resources version synced to stdlib. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index abce09af..6df3bf61 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ were contributed to different versions in the standard library: * - importlib_resources - stdlib - * - 5.8 + * - 5.9 - 3.12 * - 5.7 - 3.11 From a4b3ef808e88981efd32a7d7eb7de5288329651a Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Tue, 1 Nov 2022 11:15:12 +0530 Subject: [PATCH 213/496] Fix ResourceWarning due to unclosed file resource. --- importlib_resources/_common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 9f19784d..6a338c61 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -203,5 +203,6 @@ def _write_contents(target, source): for item in source.iterdir(): _write_contents(child, item) else: - child.open('wb').write(source.read_bytes()) + with child.open('wb') as fp: + fp.write(source.read_bytes()) return child From da84e5c7dabacf379165a0829b2f1741060ee2c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Nov 2022 05:25:30 -0500 Subject: [PATCH 214/496] 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 215/496] 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 216/496] 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 217/496] 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 218/496] 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 45edbc1a5c7b1924042a1eea114da541a5d16d07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Dec 2022 08:52:54 -0500 Subject: [PATCH 219/496] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7e0225fe..f986136d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.10.1 +======= + +* #274: Fixed ``ResourceWarning`` in ``_common``. + v5.10.0 ======= From 9708c37ef0d286c4e907adc59f46cc92262e3bf1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Dec 2022 09:10:17 -0500 Subject: [PATCH 220/496] 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 221/496] 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 222/496] 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 223/496] 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 224/496] 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 225/496] 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 d62307884400f33777dd4430288816d417b5a332 Mon Sep 17 00:00:00 2001 From: Samet Yaslan Date: Wed, 28 Dec 2022 17:21:38 +0100 Subject: [PATCH 226/496] Cherry-pick write_bytes technique from gh-100586. --- importlib_resources/_common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 6a338c61..3c6de1cf 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -203,6 +203,5 @@ def _write_contents(target, source): for item in source.iterdir(): _write_contents(child, item) else: - with child.open('wb') as fp: - fp.write(source.read_bytes()) + child.write_bytes(source.read_bytes()) return child From af2280044e2828ce51efcba9e5c22afe6dba9e37 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 28 Dec 2022 16:28:40 -0500 Subject: [PATCH 227/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f986136d..c688b1c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.10.2 +======= + +* #274: Prefer ``write_bytes`` to context manager as + proposed in gh-100586. + v5.10.1 ======= From 222aaed500d503c29792539e137fb4f69e8b2a03 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 6 Oct 2022 18:27:51 -0700 Subject: [PATCH 228/496] gh-82874: Convert remaining importlib format uses to f-str. (#98005) f-yes --- importlib_resources/_adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/_adapters.py b/importlib_resources/_adapters.py index ea363d86..f22f6bc5 100644 --- a/importlib_resources/_adapters.py +++ b/importlib_resources/_adapters.py @@ -35,7 +35,7 @@ def _io_wrapper(file, mode='r', *args, **kwargs): elif mode == 'rb': return file raise ValueError( - "Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode) + f"Invalid mode value '{mode}', only 'r' and 'rb' are supported" ) From 3ce5cc70f1d67b0ff9d87082de1d422c0e5b76e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Dec 2022 18:18:01 -0500 Subject: [PATCH 229/496] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_resources/_adapters.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/importlib_resources/_adapters.py b/importlib_resources/_adapters.py index f22f6bc5..50688fbb 100644 --- a/importlib_resources/_adapters.py +++ b/importlib_resources/_adapters.py @@ -34,9 +34,7 @@ def _io_wrapper(file, mode='r', *args, **kwargs): return TextIOWrapper(file, *args, **kwargs) elif mode == 'rb': return file - raise ValueError( - f"Invalid mode value '{mode}', only 'r' and 'rb' are supported" - ) + raise ValueError(f"Invalid mode value '{mode}', only 'r' and 'rb' are supported") class CompatibilityFiles: From a2be8da546c5ad803aea0eab34876e9b8aa0e2c8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Dec 2022 18:16:56 -0500 Subject: [PATCH 230/496] Prefer docstrings to comments where appropriate. --- importlib_resources/readers.py | 6 +++-- importlib_resources/tests/test_open.py | 4 +++- importlib_resources/tests/test_path.py | 15 ++++++++---- importlib_resources/tests/test_read.py | 4 +++- importlib_resources/tests/test_resource.py | 10 +++++--- importlib_resources/tests/util.py | 28 +++++++++++++++------- 6 files changed, 47 insertions(+), 20 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index ab34db74..c5d435f4 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -41,8 +41,10 @@ def open_resource(self, resource): raise FileNotFoundError(exc.args[0]) def is_resource(self, path): - # workaround for `zipfile.Path.is_file` returning true - # for non-existent paths. + """ + Workaround for `zipfile.Path.is_file` returning true + for non-existent paths. + """ target = self.files().joinpath(path) return target.is_file() and target.exists() diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 87b42c3d..ebd29920 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -39,7 +39,9 @@ def test_open_text_given_encoding(self): self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_open_text_with_errors(self): - # Raises UnicodeError without the 'errors' argument. + """ + Raises UnicodeError without the 'errors' argument. + """ target = resources.files(self.data) / 'utf-16.file' with target.open(encoding='utf-8', errors='strict') as fp: self.assertRaises(UnicodeError, fp.read) diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index 4f4d3943..7cb20003 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -14,9 +14,12 @@ def execute(self, package, path): class PathTests: def test_reading(self): - # Path should be readable. - # Test also implicitly verifies the returned object is a pathlib.Path - # instance. + """ + Path should be readable. + + Test also implicitly verifies the returned object is a pathlib.Path + instance. + """ target = resources.files(self.data) / 'utf-8.file' with resources.as_file(target) as path: self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) @@ -53,8 +56,10 @@ def setUp(self): class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): def test_remove_in_context_manager(self): - # It is not an error if the file that was temporarily stashed on the - # file system is removed inside the `with` stanza. + """ + It is not an error if the file that was temporarily stashed on the + file system is removed inside the `with` stanza. + """ target = resources.files(self.data) / 'utf-8.file' with resources.as_file(target) as path: path.unlink() diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 41dd6db5..b9f96abd 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -34,7 +34,9 @@ def test_read_text_given_encoding(self): self.assertEqual(result, 'Hello, UTF-16 world!\n') def test_read_text_with_errors(self): - # Raises UnicodeError without the 'errors' argument. + """ + Raises UnicodeError without the 'errors' argument. + """ target = resources.files(self.data) / 'utf-16.file' self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') result = target.read_text(encoding='utf-8', errors='ignore') diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 82390271..31ec1b63 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -69,10 +69,12 @@ def test_resource_missing(self): class ResourceCornerCaseTests(unittest.TestCase): def test_package_has_no_reader_fallback(self): - # Test odd ball packages which: + """ + Test odd ball packages which: # 1. Do not have a ResourceReader as a loader # 2. Are not on the file system # 3. Are not in a zip file + """ module = util.create_package( file=data01, path=data01.__file__, contents=['A', 'B', 'C'] ) @@ -197,8 +199,10 @@ def test_as_file_does_not_keep_open(self): # pragma: no cover del c def test_entered_path_does_not_keep_open(self): - # This is what certifi does on import to make its bundle - # available for the process duration. + """ + Mimic what certifi does on import to make its bundle + available for the process duration. + """ c = resources.as_file( resources.files('ziptestdata') / 'binary.file' ).__enter__() diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index b596c0ce..ce0e6fad 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -80,32 +80,44 @@ def execute(self, package, path): """ def test_package_name(self): - # Passing in the package name should succeed. + """ + Passing in the package name should succeed. + """ self.execute(data01.__name__, 'utf-8.file') def test_package_object(self): - # Passing in the package itself should succeed. + """ + Passing in the package itself should succeed. + """ self.execute(data01, 'utf-8.file') def test_string_path(self): - # Passing in a string for the path should succeed. + """ + Passing in a string for the path should succeed. + """ path = 'utf-8.file' self.execute(data01, path) def test_pathlib_path(self): - # Passing in a pathlib.PurePath object for the path should succeed. + """ + Passing in a pathlib.PurePath object for the path should succeed. + """ path = pathlib.PurePath('utf-8.file') self.execute(data01, path) def test_importing_module_as_side_effect(self): - # The anchor package can already be imported. + """ + The anchor package can already be imported. + """ del sys.modules[data01.__name__] self.execute(data01.__name__, 'utf-8.file') def test_missing_path(self): - # Attempting to open or read or request the path for a - # non-existent path should succeed if open_resource - # can return a viable data stream. + """ + Attempting to open or read or request the path for a + non-existent path should succeed if open_resource + can return a viable data stream. + """ bytes_data = io.BytesIO(b'Hello, world!') package = create_package(file=bytes_data, path=FileNotFoundError()) self.execute(package, 'utf-8.file') From eb2bdc83a7d3cfd1c2bc3aeae39a900d654a6839 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2023 03:17:24 -0500 Subject: [PATCH 231/496] 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 232/496] 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 233/496] 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 234/496] 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 235/496] 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 c4acc7070c8271b6afc861c0847cc09e27fcdd84 Mon Sep 17 00:00:00 2001 From: Joost Ellerbroek Date: Thu, 20 Oct 2022 21:29:04 +0200 Subject: [PATCH 236/496] Add test for MultiplexedPath.joinpath with common subdirs --- .../tests/data02/subdirectory/subsubdir/resource.txt | 1 + importlib_resources/tests/test_reader.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt diff --git a/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt b/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt new file mode 100644 index 00000000..48f587a2 --- /dev/null +++ b/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt @@ -0,0 +1 @@ +a resource \ No newline at end of file diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 1c8ebeeb..e2bdf19c 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -81,6 +81,17 @@ def test_join_path_compound(self): path = MultiplexedPath(self.folder) assert not path.joinpath('imaginary/foo.py').exists() + def test_join_path_common_subdir(self): + prefix = os.path.abspath(os.path.join(__file__, '..')) + data01 = os.path.join(prefix, 'data01') + data02 = os.path.join(prefix, 'data02') + path = MultiplexedPath(data01, data02) + self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) + self.assertEqual( + str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :], + os.path.join('data02', 'subdirectory', 'subsubdir'), + ) + def test_repr(self): self.assertEqual( repr(MultiplexedPath(self.folder)), From 35e7ebcecb27fb650a9568c06ead3fc43eba9e64 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Feb 2023 13:49:47 -0500 Subject: [PATCH 237/496] In MultiplexedPath.iterdir, honor multiple subdirectories of the same name. --- importlib_resources/_itertools.py | 35 ------------------------------- importlib_resources/readers.py | 29 ++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 38 deletions(-) delete mode 100644 importlib_resources/_itertools.py diff --git a/importlib_resources/_itertools.py b/importlib_resources/_itertools.py deleted file mode 100644 index cce05582..00000000 --- a/importlib_resources/_itertools.py +++ /dev/null @@ -1,35 +0,0 @@ -from itertools import filterfalse - -from typing import ( - Callable, - Iterable, - Iterator, - Optional, - Set, - TypeVar, - Union, -) - -# Type and type variable definitions -_T = TypeVar('_T') -_U = TypeVar('_U') - - -def unique_everseen( - iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None -) -> Iterator[_T]: - "List unique elements, preserving order. Remember all elements ever seen." - # unique_everseen('AAAABBBCCDAABBB') --> A B C D - # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen: Set[Union[_T, _U]] = set() - seen_add = seen.add - if key is None: - for element in filterfalse(seen.__contains__, iterable): - seen_add(element) - yield element - else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index c5d435f4..ab4bbea7 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,10 +1,11 @@ import collections +import contextlib +import itertools import pathlib import operator from . import abc -from ._itertools import unique_everseen from ._compat import ZipPath @@ -69,8 +70,10 @@ def __init__(self, *paths): raise NotADirectoryError('MultiplexedPath only supports directories') def iterdir(self): - files = (file for path in self._paths for file in path.iterdir()) - return unique_everseen(files, key=operator.attrgetter('name')) + children = (child for path in self._paths for child in path.iterdir()) + by_name = operator.attrgetter('name') + groups = itertools.groupby(sorted(children, key=by_name), key=by_name) + return map(self._maybe, (locs for name, locs in groups)) def read_bytes(self): raise FileNotFoundError(f'{self} is not a file') @@ -92,6 +95,26 @@ def joinpath(self, *descendants): # Just return something that will not exist. return self._paths[0].joinpath(*descendants) + @classmethod + def _maybe(cls, subdirs): + """ + Construct a MultiplexedPath if needed. + + If subdirs contains a sole element, return it. + Otherwise, return a MultiplexedPath of the items. + """ + saved = list(subdirs) + + try: + result = cls(*saved) + except NotADirectoryError: + return saved[0] + + with contextlib.suppress(ValueError): + (result,) = result._paths + + return result + def open(self, *args, **kwargs): raise FileNotFoundError(f'{self} is not a file') From 438af1586ebb382a4ad3f583a5fcd0c519b2d93a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Feb 2023 17:21:23 -0500 Subject: [PATCH 238/496] Prefer tee and only. --- importlib_resources/_itertools.py | 38 +++++++++++++++++++++++++++++++ importlib_resources/readers.py | 25 ++++++++++---------- 2 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 importlib_resources/_itertools.py diff --git a/importlib_resources/_itertools.py b/importlib_resources/_itertools.py new file mode 100644 index 00000000..7b775ef5 --- /dev/null +++ b/importlib_resources/_itertools.py @@ -0,0 +1,38 @@ +# from more_itertools 9.0 +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + """ + it = iter(iterable) + first_value = next(it, default) + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index ab4bbea7..51d030a6 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,11 +1,11 @@ import collections -import contextlib import itertools import pathlib import operator from . import abc +from ._itertools import only from ._compat import ZipPath @@ -73,7 +73,7 @@ def iterdir(self): children = (child for path in self._paths for child in path.iterdir()) by_name = operator.attrgetter('name') groups = itertools.groupby(sorted(children, key=by_name), key=by_name) - return map(self._maybe, (locs for name, locs in groups)) + return map(self._follow, (locs for name, locs in groups)) def read_bytes(self): raise FileNotFoundError(f'{self} is not a file') @@ -96,24 +96,23 @@ def joinpath(self, *descendants): return self._paths[0].joinpath(*descendants) @classmethod - def _maybe(cls, subdirs): + def _follow(cls, children): """ Construct a MultiplexedPath if needed. - If subdirs contains a sole element, return it. + If children contains a sole element, return it. Otherwise, return a MultiplexedPath of the items. + Unless one of the items is not a Directory, then return the first. """ - saved = list(subdirs) + subdirs, one_dir, one_file = itertools.tee(children, 3) try: - result = cls(*saved) - except NotADirectoryError: - return saved[0] - - with contextlib.suppress(ValueError): - (result,) = result._paths - - return result + return only(one_dir) + except ValueError: + try: + return cls(*subdirs) + except NotADirectoryError: + return next(one_file) def open(self, *args, **kwargs): raise FileNotFoundError(f'{self} is not a file') From 7fb0260f7c7dd068be176207de8b5a875179c221 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Feb 2023 20:16:51 -0500 Subject: [PATCH 239/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c688b1c6..6d76f67a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.11.0 +======= + +* #265: ``MultiplexedPath`` now honors multiple subdirectories + in ``iterdir`` and ``joinpath``. + v5.10.2 ======= From f123039aa13d9e57fee72fa82d809c4748f39f3c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Feb 2023 20:49:05 -0500 Subject: [PATCH 240/496] Add section on Namespace Packages. Fixes #262. --- docs/using.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 8464304d..0c66810f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -122,6 +122,27 @@ example could also be written as:: eml = files(email.tests.data).joinpath('message.eml').read_text() +Namespace Packages +================== + +``importlib_resources`` supports namespace packages as anchors just like +any other package. Similar to modules in a namespace package, +resources in a namespace package are not allowed to collide by name. +For example, if two packages both expose ``nspkg/data/foo.txt``, those +resources are unsupported by this library. The package will also likely +experience problems due to the collision with installers. + +It's perfectly valid, however, for two packages to present different resources +in the same namespace package, regular package, or subdirectory. +For example, one package could expose ``nspkg/data/foo.txt`` and another +expose ``nspkg/data/bar.txt`` and those two packages could be installed +into separate paths, and the resources should be queryable:: + + data = importlib_resources.files('nspkg').joinpath('data') + data.joinpath('foo.txt').read_text() + data.joinpath('bar.txt').read_text() + + File system or zip file ======================= From 5f095d18d76f7ae36e57fa3241da341b0f9cd365 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Feb 2023 09:54:15 -0500 Subject: [PATCH 241/496] 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 242/496] 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 243/496] 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 3289eb45ec2e2bfef667da1c33988713bdb2679a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 19:32:55 -0500 Subject: [PATCH 244/496] Remove 'absolute' call, unneeded. --- importlib_resources/tests/test_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 31ec1b63..d29228aa 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -154,7 +154,7 @@ def setUp(self): data_path = pathlib.Path(self.ZIP_MODULE.__file__) data_dir = data_path.parent self.source_zip_path = data_dir / 'ziptestdata.zip' - self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip').absolute() + self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip') self.zip_path.write_bytes(self.source_zip_path.read_bytes()) sys.path.append(str(self.zip_path)) self.data = import_module('ziptestdata') From b8060130bf72b5170baa8a29493d4baf4f31cca5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 20:00:40 -0500 Subject: [PATCH 245/496] Extract fixture for zip_on_path. --- importlib_resources/tests/test_resource.py | 87 ++++++++++------------ 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index d29228aa..97dd39a6 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -1,3 +1,4 @@ +import contextlib import sys import unittest import importlib_resources as resources @@ -8,7 +9,7 @@ from . import zipdata01, zipdata02 from . import util from importlib import import_module -from ._compat import import_helper, unlink +from ._compat import import_helper, os_helper, unlink class ResourceTests: @@ -140,84 +141,76 @@ def test_unrelated_contents(self): ) -class DeletingZipsTest(unittest.TestCase): - """Having accessed resources in a zip file should not keep an open - reference to the zip. - """ - - ZIP_MODULE = zipdata01 +@contextlib.contextmanager +def zip_on_path(dir): + data_path = pathlib.Path(zipdata01.__file__) + source_zip_path = data_path.parent.joinpath('ziptestdata.zip') + zip_path = pathlib.Path(dir) / f'{uuid.uuid4()}.zip' + zip_path.write_bytes(source_zip_path.read_bytes()) + sys.path.append(str(zip_path)) + import_module('ziptestdata') - def setUp(self): - modules = import_helper.modules_setup() - self.addCleanup(import_helper.modules_cleanup, *modules) - - data_path = pathlib.Path(self.ZIP_MODULE.__file__) - data_dir = data_path.parent - self.source_zip_path = data_dir / 'ziptestdata.zip' - self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip') - self.zip_path.write_bytes(self.source_zip_path.read_bytes()) - sys.path.append(str(self.zip_path)) - self.data = import_module('ziptestdata') - - def tearDown(self): + try: + yield + finally: try: - sys.path.remove(str(self.zip_path)) + sys.path.remove(str(zip_path)) except ValueError: pass try: - del sys.path_importer_cache[str(self.zip_path)] - del sys.modules[self.data.__name__] + del sys.path_importer_cache[str(zip_path)] + del sys.modules['ziptestdata'] except KeyError: pass try: - unlink(self.zip_path) + unlink(zip_path) except OSError: # If the test fails, this will probably fail too pass + +class DeletingZipsTest(unittest.TestCase): + """Having accessed resources in a zip file should not keep an open + reference to the zip. + """ + + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + self.fixtures.enter_context(zip_on_path(temp_dir)) + def test_iterdir_does_not_keep_open(self): - c = [item.name for item in resources.files('ziptestdata').iterdir()] - self.zip_path.unlink() - del c + [item.name for item in resources.files('ziptestdata').iterdir()] def test_is_file_does_not_keep_open(self): - c = resources.files('ziptestdata').joinpath('binary.file').is_file() - self.zip_path.unlink() - del c + resources.files('ziptestdata').joinpath('binary.file').is_file() def test_is_file_failure_does_not_keep_open(self): - c = resources.files('ziptestdata').joinpath('not-present').is_file() - self.zip_path.unlink() - del c + resources.files('ziptestdata').joinpath('not-present').is_file() @unittest.skip("Desired but not supported.") def test_as_file_does_not_keep_open(self): # pragma: no cover - c = resources.as_file(resources.files('ziptestdata') / 'binary.file') - self.zip_path.unlink() - del c + resources.as_file(resources.files('ziptestdata') / 'binary.file') def test_entered_path_does_not_keep_open(self): """ Mimic what certifi does on import to make its bundle available for the process duration. """ - c = resources.as_file( - resources.files('ziptestdata') / 'binary.file' - ).__enter__() - self.zip_path.unlink() - del c + resources.as_file(resources.files('ziptestdata') / 'binary.file').__enter__() def test_read_binary_does_not_keep_open(self): - c = resources.files('ziptestdata').joinpath('binary.file').read_bytes() - self.zip_path.unlink() - del c + resources.files('ziptestdata').joinpath('binary.file').read_bytes() def test_read_text_does_not_keep_open(self): - c = resources.files('ziptestdata').joinpath('utf-8.file').read_text() - self.zip_path.unlink() - del c + resources.files('ziptestdata').joinpath('utf-8.file').read_text() class ResourceFromNamespaceTest01(unittest.TestCase): From 0a9b721b9748da6e96a6d1123392a4ad5819f79d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 20:08:26 -0500 Subject: [PATCH 246/496] Prefer suppress to except/pass. --- importlib_resources/tests/test_resource.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 97dd39a6..76823313 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -153,22 +153,15 @@ def zip_on_path(dir): try: yield finally: - try: + with contextlib.suppress(ValueError): sys.path.remove(str(zip_path)) - except ValueError: - pass - try: + with contextlib.suppress(KeyError): del sys.path_importer_cache[str(zip_path)] del sys.modules['ziptestdata'] - except KeyError: - pass - try: + with contextlib.suppress(OSError): unlink(zip_path) - except OSError: - # If the test fails, this will probably fail too - pass class DeletingZipsTest(unittest.TestCase): From 56cdf46aa19450d58b4a56af6553a0225762ae4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 21:12:36 -0500 Subject: [PATCH 247/496] 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 a176eb97643f4af8c0f597bd806c01677339fc71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 21:39:14 -0500 Subject: [PATCH 248/496] Update _path.build/create from jaraco.path 3.4.1. Fixes EncodingWarnings. --- importlib_resources/tests/_path.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/importlib_resources/tests/_path.py b/importlib_resources/tests/_path.py index c630e4d3..1f97c961 100644 --- a/importlib_resources/tests/_path.py +++ b/importlib_resources/tests/_path.py @@ -1,12 +1,16 @@ import pathlib import functools +from typing import Dict, Union + #### -# from jaraco.path 3.4 +# from jaraco.path 3.4.1 + +FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore -def build(spec, prefix=pathlib.Path()): +def build(spec: FilesSpec, prefix=pathlib.Path()): """ Build a set of files/directories, as described by the spec. @@ -23,15 +27,17 @@ def build(spec, prefix=pathlib.Path()): ... "baz.py": "# Some code", ... } ... } - >>> tmpdir = getfixture('tmpdir') - >>> build(spec, tmpdir) + >>> target = getfixture('tmp_path') + >>> build(spec, target) + >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') + '# Some code' """ for name, contents in spec.items(): create(contents, pathlib.Path(prefix) / name) @functools.singledispatch -def create(content, path): +def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) build(content, prefix=path) # type: ignore @@ -43,7 +49,7 @@ def _(content: bytes, path): @create.register def _(content: str, path): - path.write_text(content) + path.write_text(content, encoding='utf-8') # end from jaraco.path From 0dd691bbcb24907c84c856a0b9a5f25c38ea9629 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 21:48:56 -0500 Subject: [PATCH 249/496] Fix EncodingWarnings --- importlib_resources/tests/test_compatibilty_files.py | 6 ++++-- importlib_resources/tests/test_files.py | 4 ++-- importlib_resources/tests/test_open.py | 4 ++-- importlib_resources/tests/test_read.py | 8 ++++++-- importlib_resources/tests/test_resource.py | 4 +++- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/test_compatibilty_files.py b/importlib_resources/tests/test_compatibilty_files.py index d92c7c56..13ad0dfb 100644 --- a/importlib_resources/tests/test_compatibilty_files.py +++ b/importlib_resources/tests/test_compatibilty_files.py @@ -64,11 +64,13 @@ def test_orphan_path_name(self): def test_spec_path_open(self): self.assertEqual(self.files.read_bytes(), b'Hello, world!') - self.assertEqual(self.files.read_text(), 'Hello, world!') + self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!') def test_child_path_open(self): self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') - self.assertEqual((self.files / 'a').read_text(), 'Hello, world!') + self.assertEqual( + (self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!' + ) def test_orphan_path_open(self): with self.assertRaises(FileNotFoundError): diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index d258fb5f..197a063b 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -84,7 +84,7 @@ def test_module_resources(self): _path.build(spec, self.site_dir) import mod - actual = resources.files(mod).joinpath('res.txt').read_text() + actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') assert actual == spec['res.txt'] @@ -98,7 +98,7 @@ def test_implicit_files(self): '__init__.py': textwrap.dedent( """ import importlib_resources as res - val = res.files().joinpath('res.txt').read_text() + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') """ ), 'res.txt': 'resources are the best', diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index ebd29920..ed08672d 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -15,7 +15,7 @@ def execute(self, package, path): class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): target = resources.files(package).joinpath(path) - with target.open(): + with target.open(encoding='utf-8'): pass @@ -28,7 +28,7 @@ def test_open_binary(self): def test_open_text_default_encoding(self): target = resources.files(self.data) / 'utf-8.file' - with target.open() as fp: + with target.open(encoding='utf-8') as fp: result = fp.read() self.assertEqual(result, 'Hello, UTF-8 world!\n') diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index b9f96abd..b5aaec45 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -13,7 +13,7 @@ def execute(self, package, path): class CommonTextTests(util.CommonTests, unittest.TestCase): def execute(self, package, path): - resources.files(package).joinpath(path).read_text() + resources.files(package).joinpath(path).read_text(encoding='utf-8') class ReadTests: @@ -22,7 +22,11 @@ def test_read_bytes(self): self.assertEqual(result, b'\0\1\2\3') def test_read_text_default_encoding(self): - result = resources.files(self.data).joinpath('utf-8.file').read_text() + result = ( + resources.files(self.data) + .joinpath('utf-8.file') + .read_text(encoding='utf-8') + ) self.assertEqual(result, 'Hello, UTF-8 world!\n') def test_read_text_given_encoding(self): diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 76823313..677110cd 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -203,7 +203,9 @@ def test_read_binary_does_not_keep_open(self): resources.files('ziptestdata').joinpath('binary.file').read_bytes() def test_read_text_does_not_keep_open(self): - resources.files('ziptestdata').joinpath('utf-8.file').read_text() + resources.files('ziptestdata').joinpath('utf-8.file').read_text( + encoding='utf-8' + ) class ResourceFromNamespaceTest01(unittest.TestCase): From ff16bd3adf8060ad0c6e39a17ac1ccbf67b9bda2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 13:07:49 -0500 Subject: [PATCH 250/496] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c688b1c6..878dc024 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.10.3 +======= + +* Packaging refresh, including fixing EncodingWarnings + and some tests cleanup. + v5.10.2 ======= From 28d26e860a88a6255a729c50259c4662cd86fbf2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 13:33:31 -0500 Subject: [PATCH 251/496] Pass 'encoding' to target.open in test_open_text_FileNotFoundError. Fixes #280. --- CHANGES.rst | 5 +++++ importlib_resources/tests/test_open.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 878dc024..9035373e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.10.4 +======= + +* #280: Fixed one more ``EncodingWarning`` in test suite. + v5.10.3 ======= diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index ed08672d..83b737dc 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -56,11 +56,13 @@ def test_open_text_with_errors(self): def test_open_binary_FileNotFoundError(self): target = resources.files(self.data) / 'does-not-exist' - self.assertRaises(FileNotFoundError, target.open, 'rb') + with self.assertRaises(FileNotFoundError): + target.open('rb') def test_open_text_FileNotFoundError(self): target = resources.files(self.data) / 'does-not-exist' - self.assertRaises(FileNotFoundError, target.open) + with self.assertRaises(FileNotFoundError): + target.open(encoding='utf-8') class OpenDiskTests(OpenTests, unittest.TestCase): From 1f0a098cb8ca0e9e7c8dad76e7cac583b586f709 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 13:40:26 -0500 Subject: [PATCH 252/496] Update changelog. --- CHANGES.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 381871bb..5db29c10 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,14 +1,17 @@ -v5.11.0 +v5.11.1 ======= -* #265: ``MultiplexedPath`` now honors multiple subdirectories - in ``iterdir`` and ``joinpath``. - v5.10.4 ======= * #280: Fixed one more ``EncodingWarning`` in test suite. +v5.11.0 +======= + +* #265: ``MultiplexedPath`` now honors multiple subdirectories + in ``iterdir`` and ``joinpath``. + v5.10.3 ======= From 2bd9048421fcf044adb080c738ddbc7b46545168 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 16:32:57 -0500 Subject: [PATCH 253/496] Add tests capturing expectation of custom loaders. Ref #268. --- importlib_resources/tests/test_custom.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 importlib_resources/tests/test_custom.py diff --git a/importlib_resources/tests/test_custom.py b/importlib_resources/tests/test_custom.py new file mode 100644 index 00000000..e85ddd65 --- /dev/null +++ b/importlib_resources/tests/test_custom.py @@ -0,0 +1,45 @@ +import unittest +import contextlib +import pathlib + +import importlib_resources as resources +from ..abc import TraversableResources, ResourceReader +from . import util +from ._compat import os_helper + + +class SimpleLoader: + """ + A simple loader that only implements a resource reader. + """ + + def __init__(self, reader: ResourceReader): + self.reader = reader + + def get_resource_reader(self, package): + return self.reader + + +class MagicResources(TraversableResources): + """ + Magically returns the resources at path. + """ + + def __init__(self, path: pathlib.Path): + self.path = path + + def files(self): + return self.path + + +class CustomTraversableResourcesTests(unittest.TestCase): + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + + def test_custom_loader(self): + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + loader = SimpleLoader(MagicResources(temp_dir)) + pkg = util.create_package_from_loader(loader) + files = resources.files(pkg) + assert files is temp_dir From 68be8b432051731aae620297e42660bd547d5551 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 16:45:06 -0400 Subject: [PATCH 254/496] Disable native reader even if it supplies files, because it might not have features/fixes present in this library. Fixes #257. --- importlib_resources/_compat.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 8d7ade08..5dc1f288 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -72,9 +72,6 @@ def _file_reader(spec): return readers.FileReader(self) return ( - # native reader if it supplies 'files' - _native_reader(self.spec) - or # local ZipReader if a zip module _zip_reader(self.spec) or From 8da2027c506edd294445c93bd810f760696185a3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 16:44:54 -0500 Subject: [PATCH 255/496] Give the local zip/namespace/file readers precedence over the native reader. --- importlib_resources/_compat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 5dc1f288..a93a8826 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -80,8 +80,12 @@ def _file_reader(spec): or # local FileReader _file_reader(self.spec) + or + # native reader if it supplies 'files' + _native_reader(self.spec) + or # fallback - adapt the spec ResourceReader to TraversableReader - or _adapters.CompatibilityFiles(self.spec) + _adapters.CompatibilityFiles(self.spec) ) From f5af90874fd0e76f92e9e5d27a4c997c1a87e7aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Feb 2023 17:25:45 -0500 Subject: [PATCH 256/496] Update changelog. --- CHANGES.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f986136d..a0ced4ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +v5.12.0 +======= + +* #257: ``importlib_resources`` (backport) now gives + precedence to built-in readers (file system, zip, + namespace packages), providing forward-compatibility + of behaviors like ``MultiplexedPath``. + v5.10.1 ======= From 109f8c09ddb4904dc3f83307473520b2250ccb30 Mon Sep 17 00:00:00 2001 From: Joyce Date: Sat, 18 Mar 2023 13:25:16 -0300 Subject: [PATCH 257/496] 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 258/496] 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 259/496] 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 260/496] 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 261/496] 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 262/496] 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 263/496] 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 264/496] 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 265/496] 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 266/496] 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 267/496] 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 268/496] 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 269/496] 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 270/496] 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 271/496] 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 272/496] 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 273/496] 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 274/496] 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 275/496] 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 0de39943e6ab57598618018711d44d1f8c71c80d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 12:14:12 -0400 Subject: [PATCH 276/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+drop-py37.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 387af945..bad8dfb6 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v5.13.0 +======= + +Features +-------- + +- Require Python 3.8 or later. + + v5.12.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. From 224d77ee69426746351d26e67abca486002f1282 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 12:31:29 -0400 Subject: [PATCH 277/496] Remove legacy support. Fixes #80. --- importlib_resources/__init__.py | 19 ----- importlib_resources/_legacy.py | 120 -------------------------------- newsfragments/80.removal.rst | 1 + 3 files changed, 1 insertion(+), 139 deletions(-) delete mode 100644 importlib_resources/_legacy.py create mode 100644 newsfragments/80.removal.rst diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 34e3a995..e6b60c18 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -6,31 +6,12 @@ Package, ) -from ._legacy import ( - contents, - open_binary, - read_binary, - open_text, - read_text, - is_resource, - path, - Resource, -) - from .abc import ResourceReader __all__ = [ 'Package', - 'Resource', 'ResourceReader', 'as_file', - 'contents', 'files', - 'is_resource', - 'open_binary', - 'open_text', - 'path', - 'read_binary', - 'read_text', ] diff --git a/importlib_resources/_legacy.py b/importlib_resources/_legacy.py deleted file mode 100644 index b1ea8105..00000000 --- a/importlib_resources/_legacy.py +++ /dev/null @@ -1,120 +0,0 @@ -import functools -import os -import pathlib -import types -import warnings - -from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any - -from . import _common - -Package = Union[types.ModuleType, str] -Resource = str - - -def deprecated(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - warnings.warn( - f"{func.__name__} is deprecated. Use files() instead. " - "Refer to https://importlib-resources.readthedocs.io" - "/en/latest/using.html#migrating-from-legacy for migration advice.", - DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return wrapper - - -def normalize_path(path: Any) -> str: - """Normalize a path by ensuring it is a string. - - If the resulting string contains path separators, an exception is raised. - """ - str_path = str(path) - parent, file_name = os.path.split(str_path) - if parent: - raise ValueError(f'{path!r} must be only a file name') - return file_name - - -@deprecated -def open_binary(package: Package, resource: Resource) -> BinaryIO: - """Return a file-like object opened for binary reading of the resource.""" - return (_common.files(package) / normalize_path(resource)).open('rb') - - -@deprecated -def read_binary(package: Package, resource: Resource) -> bytes: - """Return the binary contents of the resource.""" - return (_common.files(package) / normalize_path(resource)).read_bytes() - - -@deprecated -def open_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> TextIO: - """Return a file-like object opened for text reading of the resource.""" - return (_common.files(package) / normalize_path(resource)).open( - 'r', encoding=encoding, errors=errors - ) - - -@deprecated -def read_text( - package: Package, - resource: Resource, - encoding: str = 'utf-8', - errors: str = 'strict', -) -> str: - """Return the decoded string of the resource. - - The decoding-related arguments have the same semantics as those of - bytes.decode(). - """ - with open_text(package, resource, encoding, errors) as fp: - return fp.read() - - -@deprecated -def contents(package: Package) -> Iterable[str]: - """Return an iterable of entries in `package`. - - Note that not all entries are resources. Specifically, directories are - not considered resources. Use `is_resource()` on each entry returned here - to check if it is a resource or not. - """ - return [path.name for path in _common.files(package).iterdir()] - - -@deprecated -def is_resource(package: Package, name: str) -> bool: - """True if `name` is a resource inside `package`. - - Directories are *not* resources. - """ - resource = normalize_path(name) - return any( - traversable.name == resource and traversable.is_file() - for traversable in _common.files(package).iterdir() - ) - - -@deprecated -def path( - package: Package, - resource: Resource, -) -> ContextManager[pathlib.Path]: - """A context manager providing a file path object to the resource. - - If the resource does not already exist on its own on the file system, - a temporary file will be created. If the file was created, the file - will be deleted upon exiting the context manager (no exception is - raised if the file was deleted prior to the context manager - exiting). - """ - return _common.as_file(_common.files(package) / normalize_path(resource)) diff --git a/newsfragments/80.removal.rst b/newsfragments/80.removal.rst new file mode 100644 index 00000000..42a29011 --- /dev/null +++ b/newsfragments/80.removal.rst @@ -0,0 +1 @@ +Removed legacy functions deprecated in 5.3. From 57a03d67f485c252d94071be0403aa191cc04bdf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 14:15:50 -0400 Subject: [PATCH 278/496] Update Python version mapping to include Python 3.13 and updates to 3.12 per python/cpython#97930. --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 994ea052..2bfbb3f0 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,9 @@ were contributed to different versions in the standard library: * - importlib_resources - stdlib - * - 5.9 + * - 6.0 + - 3.13 + * - 5.12 - 3.12 * - 5.7 - 3.11 From a3f5a0b5337c1b824271578659a133c7cba6d3a6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 14:17:13 -0400 Subject: [PATCH 279/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/80.removal.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/80.removal.rst diff --git a/NEWS.rst b/NEWS.rst index bad8dfb6..e654c91a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.0.0 +====== + +Deprecations and Removals +------------------------- + +- Removed legacy functions deprecated in 5.3. (#80) + + v5.13.0 ======= diff --git a/newsfragments/80.removal.rst b/newsfragments/80.removal.rst deleted file mode 100644 index 42a29011..00000000 --- a/newsfragments/80.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Removed legacy functions deprecated in 5.3. From a7310562fad7a8834c9810c1edd8e00b03e1394b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 21:48:23 -0400 Subject: [PATCH 280/496] 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 281/496] 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 a203c1b26ee921bd4b17cc8e405148b382ae46aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Aug 2023 23:38:22 -0400 Subject: [PATCH 282/496] Restore LICENSE unintentionally changed in 8de19cf901dece8c587e2d072a687a7acf81d49e. Closes #285 --- LICENSE | 219 ++++++++++++++++++++++++++++++++--- newsfragments/285.bugfix.rst | 1 + 2 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 newsfragments/285.bugfix.rst diff --git a/LICENSE b/LICENSE index 1bb5a443..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,17 +1,202 @@ -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. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/newsfragments/285.bugfix.rst b/newsfragments/285.bugfix.rst new file mode 100644 index 00000000..8b4884d5 --- /dev/null +++ b/newsfragments/285.bugfix.rst @@ -0,0 +1 @@ +Restored Apache license. \ No newline at end of file From e8ba13d4f8eb977800dab7bf6582adad71c558e0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 7 Aug 2023 01:03:58 -0400 Subject: [PATCH 283/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/285.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/285.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index e654c91a..5c97c82d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.0.1 +====== + +Bugfixes +-------- + +- Restored Apache license. (#285) + + v6.0.0 ====== diff --git a/newsfragments/285.bugfix.rst b/newsfragments/285.bugfix.rst deleted file mode 100644 index 8b4884d5..00000000 --- a/newsfragments/285.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restored Apache license. \ No newline at end of file From d5399ef8f183f0ad518871036607189d59a1c1cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 7 Aug 2023 09:41:00 -0400 Subject: [PATCH 284/496] Add API docs. Closes #245. --- docs/api.rst | 26 ++++++++++++++++++++++++++ docs/conf.py | 8 ++++++++ docs/index.rst | 7 ++++--- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 docs/api.rst diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..739be9ed --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,26 @@ +============= +API Reference +============= + +``importlib_resources`` module +------------------------------ + +.. automodule:: importlib_resources + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: importlib_resources.abc + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: importlib_resources.readers + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: importlib_resources.simple + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 1500b33d..b4eaace9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ # Be strict about any broken references nitpicky = True +nitpick_ignore = [] # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 @@ -46,3 +47,10 @@ autodoc_preserve_defaults = True extensions += ['jaraco.tidelift'] + +nitpick_ignore.extend( + [ + ('py:class', 'module'), + ('py:class', '_io.BufferedReader'), + ] +) diff --git a/docs/index.rst b/docs/index.rst index 24befdc0..eee051cc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,9 +31,10 @@ The documentation here includes a general :ref:`usage ` guide and a :maxdepth: 2 :caption: Contents: - using.rst - migration.rst - history.rst + using + api + migration + history .. tidelift-referral-banner:: From 0e2032c4754c598ba75e467c64009ba4490ddea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Aug 2023 18:42:14 -0400 Subject: [PATCH 285/496] 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 286/496] 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 287/496] 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 288/496] 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 289/496] 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 290/496] 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 291/496] 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 496acc1a0d8018c830b30a3a28826c9b101975fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 12 Sep 2023 15:05:00 -0400 Subject: [PATCH 292/496] Replace static zip fixtures with dynamically generated zip fixtures built from the same modules as found on the file system. --- importlib_resources/tests/test_read.py | 6 +- importlib_resources/tests/test_resource.py | 77 ++++-------------- importlib_resources/tests/update-zips.py | 53 ------------ importlib_resources/tests/util.py | 48 ++++------- importlib_resources/tests/zip.py | 29 +++++++ .../tests/zipdata01/__init__.py | 0 .../tests/zipdata01/ziptestdata.zip | Bin 876 -> 0 bytes .../tests/zipdata02/__init__.py | 0 .../tests/zipdata02/ziptestdata.zip | Bin 698 -> 0 bytes 9 files changed, 66 insertions(+), 147 deletions(-) delete mode 100755 importlib_resources/tests/update-zips.py create mode 100755 importlib_resources/tests/zip.py delete mode 100644 importlib_resources/tests/zipdata01/__init__.py delete mode 100644 importlib_resources/tests/zipdata01/ziptestdata.zip delete mode 100644 importlib_resources/tests/zipdata02/__init__.py delete mode 100644 importlib_resources/tests/zipdata02/ziptestdata.zip diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index b5aaec45..5b83221e 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -58,15 +58,13 @@ class ReadDiskTests(ReadTests, unittest.TestCase): class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): - submodule = import_module('ziptestdata.subdirectory') + submodule = import_module('data01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() self.assertEqual(result, b'\0\1\2\3') def test_read_submodule_resource_by_name(self): result = ( - resources.files('ziptestdata.subdirectory') - .joinpath('binary.file') - .read_bytes() + resources.files('data01.subdirectory').joinpath('binary.file').read_bytes() ) self.assertEqual(result, b'\0\1\2\3') diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 677110cd..4069b6ca 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -1,15 +1,11 @@ -import contextlib import sys import unittest import importlib_resources as resources -import uuid import pathlib from . import data01 -from . import zipdata01, zipdata02 from . import util from importlib import import_module -from ._compat import import_helper, os_helper, unlink class ResourceTests: @@ -89,34 +85,32 @@ def test_package_has_no_reader_fallback(self): class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): - ZIP_MODULE = zipdata01 # type: ignore + ZIP_MODULE = 'data01' def test_is_submodule_resource(self): - submodule = import_module('ziptestdata.subdirectory') + submodule = import_module('data01.subdirectory') self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) def test_read_submodule_resource_by_name(self): self.assertTrue( - resources.files('ziptestdata.subdirectory') - .joinpath('binary.file') - .is_file() + resources.files('data01.subdirectory').joinpath('binary.file').is_file() ) def test_submodule_contents(self): - submodule = import_module('ziptestdata.subdirectory') + submodule = import_module('data01.subdirectory') self.assertEqual( names(resources.files(submodule)), {'__init__.py', 'binary.file'} ) def test_submodule_contents_by_name(self): self.assertEqual( - names(resources.files('ziptestdata.subdirectory')), + names(resources.files('data01.subdirectory')), {'__init__.py', 'binary.file'}, ) def test_as_file_directory(self): - with resources.as_file(resources.files('ziptestdata')) as data: - assert data.name == 'ziptestdata' + with resources.as_file(resources.files('data01')) as data: + assert data.name == 'data01' assert data.is_dir() assert data.joinpath('subdirectory').is_dir() assert len(list(data.iterdir())) @@ -124,7 +118,7 @@ def test_as_file_directory(self): class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): - ZIP_MODULE = zipdata02 # type: ignore + ZIP_MODULE = 'data02' def test_unrelated_contents(self): """ @@ -132,80 +126,45 @@ def test_unrelated_contents(self): distinct resources. Ref python/importlib_resources#44. """ self.assertEqual( - names(resources.files('ziptestdata.one')), + names(resources.files('data02.one')), {'__init__.py', 'resource1.txt'}, ) self.assertEqual( - names(resources.files('ziptestdata.two')), + names(resources.files('data02.two')), {'__init__.py', 'resource2.txt'}, ) -@contextlib.contextmanager -def zip_on_path(dir): - data_path = pathlib.Path(zipdata01.__file__) - source_zip_path = data_path.parent.joinpath('ziptestdata.zip') - zip_path = pathlib.Path(dir) / f'{uuid.uuid4()}.zip' - zip_path.write_bytes(source_zip_path.read_bytes()) - sys.path.append(str(zip_path)) - import_module('ziptestdata') - - try: - yield - finally: - with contextlib.suppress(ValueError): - sys.path.remove(str(zip_path)) - - with contextlib.suppress(KeyError): - del sys.path_importer_cache[str(zip_path)] - del sys.modules['ziptestdata'] - - with contextlib.suppress(OSError): - unlink(zip_path) - - -class DeletingZipsTest(unittest.TestCase): +class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase): """Having accessed resources in a zip file should not keep an open reference to the zip. """ - def setUp(self): - self.fixtures = contextlib.ExitStack() - self.addCleanup(self.fixtures.close) - - modules = import_helper.modules_setup() - self.addCleanup(import_helper.modules_cleanup, *modules) - - temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) - self.fixtures.enter_context(zip_on_path(temp_dir)) - def test_iterdir_does_not_keep_open(self): - [item.name for item in resources.files('ziptestdata').iterdir()] + [item.name for item in resources.files('data01').iterdir()] def test_is_file_does_not_keep_open(self): - resources.files('ziptestdata').joinpath('binary.file').is_file() + resources.files('data01').joinpath('binary.file').is_file() def test_is_file_failure_does_not_keep_open(self): - resources.files('ziptestdata').joinpath('not-present').is_file() + resources.files('data01').joinpath('not-present').is_file() @unittest.skip("Desired but not supported.") def test_as_file_does_not_keep_open(self): # pragma: no cover - resources.as_file(resources.files('ziptestdata') / 'binary.file') + resources.as_file(resources.files('data01') / 'binary.file') def test_entered_path_does_not_keep_open(self): """ Mimic what certifi does on import to make its bundle available for the process duration. """ - resources.as_file(resources.files('ziptestdata') / 'binary.file').__enter__() + resources.as_file(resources.files('data01') / 'binary.file').__enter__() def test_read_binary_does_not_keep_open(self): - resources.files('ziptestdata').joinpath('binary.file').read_bytes() + resources.files('data01').joinpath('binary.file').read_bytes() def test_read_text_does_not_keep_open(self): - resources.files('ziptestdata').joinpath('utf-8.file').read_text( - encoding='utf-8' - ) + resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8') class ResourceFromNamespaceTest01(unittest.TestCase): diff --git a/importlib_resources/tests/update-zips.py b/importlib_resources/tests/update-zips.py deleted file mode 100755 index 231334aa..00000000 --- a/importlib_resources/tests/update-zips.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Generate the zip test data files. - -Run to build the tests/zipdataNN/ziptestdata.zip files from -files in tests/dataNN. - -Replaces the file with the working copy, but does commit anything -to the source repo. -""" - -import contextlib -import os -import pathlib -import zipfile - - -def main(): - """ - >>> from unittest import mock - >>> monkeypatch = getfixture('monkeypatch') - >>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock()) - >>> print(); main() # print workaround for bpo-32509 - - ...data01... -> ziptestdata/... - ... - ...data02... -> ziptestdata/... - ... - """ - suffixes = '01', '02' - tuple(map(generate, suffixes)) - - -def generate(suffix): - root = pathlib.Path(__file__).parent.relative_to(os.getcwd()) - zfpath = root / f'zipdata{suffix}/ziptestdata.zip' - with zipfile.ZipFile(zfpath, 'w') as zf: - for src, rel in walk(root / f'data{suffix}'): - dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix()) - print(src, '->', dst) - zf.write(src, dst) - - -def walk(datapath): - for dirpath, dirnames, filenames in os.walk(datapath): - with contextlib.suppress(ValueError): - dirnames.remove('__pycache__') - for filename in filenames: - res = pathlib.Path(dirpath) / filename - rel = res.relative_to(datapath) - yield res, rel - - -__name__ == '__main__' and main() diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index ce0e6fad..066f4113 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -4,11 +4,12 @@ import sys import types import pathlib +import contextlib from . import data01 -from . import zipdata01 from ..abc import ResourceReader -from ._compat import import_helper +from ._compat import import_helper, os_helper +from . import zip as zip_ from importlib.machinery import ModuleSpec @@ -141,39 +142,24 @@ def test_useless_loader(self): class ZipSetupBase: - ZIP_MODULE = None - - @classmethod - def setUpClass(cls): - data_path = pathlib.Path(cls.ZIP_MODULE.__file__) - data_dir = data_path.parent - cls._zip_path = str(data_dir / 'ziptestdata.zip') - sys.path.append(cls._zip_path) - cls.data = importlib.import_module('ziptestdata') - - @classmethod - def tearDownClass(cls): - try: - sys.path.remove(cls._zip_path) - except ValueError: - pass - - try: - del sys.path_importer_cache[cls._zip_path] - del sys.modules[cls.data.__name__] - except KeyError: - pass - - try: - del cls.data - del cls._zip_path - except AttributeError: - pass + ZIP_MODULE = 'data01' def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + modules = import_helper.modules_setup() self.addCleanup(import_helper.modules_cleanup, *modules) + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + modules = pathlib.Path(temp_dir) / 'zipped modules.zip' + src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE) + self.fixtures.enter_context( + import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules))) + ) + + self.data = importlib.import_module(self.ZIP_MODULE) + class ZipSetup(ZipSetupBase): - ZIP_MODULE = zipdata01 # type: ignore + pass diff --git a/importlib_resources/tests/zip.py b/importlib_resources/tests/zip.py new file mode 100755 index 00000000..6ab914b0 --- /dev/null +++ b/importlib_resources/tests/zip.py @@ -0,0 +1,29 @@ +""" +Generate zip test data files. +""" + +import contextlib +import os +import pathlib +import zipfile + + +def make_zip_file(src, dst): + """ + Zip the files in src into a new zipfile at dst. + """ + with zipfile.ZipFile(dst, 'w') as zf: + for src_path, rel in walk(src): + dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) + zf.write(src_path, dst_name) + return dst + + +def walk(datapath): + for dirpath, dirnames, filenames in os.walk(datapath): + with contextlib.suppress(ValueError): + dirnames.remove('__pycache__') + for filename in filenames: + res = pathlib.Path(dirpath) / filename + rel = res.relative_to(datapath) + yield res, rel diff --git a/importlib_resources/tests/zipdata01/__init__.py b/importlib_resources/tests/zipdata01/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/zipdata01/ziptestdata.zip b/importlib_resources/tests/zipdata01/ziptestdata.zip deleted file mode 100644 index 9a3bb0739f87e97c1084b94d7d153680f6727738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 876 zcmWIWW@Zs#00HOCX@Q%&m27l?Y!DU);;PJolGNgol*E!m{nC;&T|+ayw9K5;|NlG~ zQWMD z9;rDw`8o=rA#S=B3g!7lIVp-}COK17UPc zNtt;*xhM-3R!jMEPhCreO-3*u>5Df}T7+BJ{639e$2uhfsIs`pJ5Qf}C xGXyDE@VNvOv@o!wQJfLgCAgysx3f@9jKpUmiW^zkK<;1z!tFpk^MROw0RS~O%0&PG diff --git a/importlib_resources/tests/zipdata02/__init__.py b/importlib_resources/tests/zipdata02/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/zipdata02/ziptestdata.zip b/importlib_resources/tests/zipdata02/ziptestdata.zip deleted file mode 100644 index d63ff512d2807ef2fd259455283b81b02e0e45fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 698 zcmWIWW@Zs#00HOCX@Ot{ln@8fRhb1Psl_EJi6x2p@$s2?nI-Y@dIgmMI5kP5Y0A$_ z#jWw|&p#`9ff_(q7K_HB)Z+ZoqU2OVy^@L&ph*fa0WRVlP*R?c+X1opI-R&20MZDv z&j{oIpa8N17@0(vaR(gGH(;=&5k%n(M%;#g0ulz6G@1gL$cA79E2=^00gEsw4~s!C zUxI@ZWaIMqz|BszK;s4KsL2<9jRy!Q2E6`2cTLHjr{wAk1ZCU@!+_ G1_l6Bc%f?m From 023d2c13e5c3598e187470369d94bcf39ed411b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 12 Sep 2023 15:37:11 -0400 Subject: [PATCH 293/496] Separate 'disk' concern of namespace tests. --- importlib_resources/tests/test_resource.py | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 4069b6ca..40368056 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -167,17 +167,7 @@ def test_read_text_does_not_keep_open(self): resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8') -class ResourceFromNamespaceTest01(unittest.TestCase): - site_dir = str(pathlib.Path(__file__).parent) - - @classmethod - def setUpClass(cls): - sys.path.append(cls.site_dir) - - @classmethod - def tearDownClass(cls): - sys.path.remove(cls.site_dir) - +class ResourceFromNamespaceTests: def test_is_submodule_resource(self): self.assertTrue( resources.files(import_module('namespacedata01')) @@ -207,5 +197,17 @@ def test_submodule_contents_by_name(self): self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) +class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + if __name__ == '__main__': unittest.main() From ca1831c2148fe5ddbffd001de76ff5f6005f812c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2023 11:05:36 -0400 Subject: [PATCH 294/496] 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 d3a7f69e44043719330fb68e6183ce0da31b47d3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2023 11:38:27 -0400 Subject: [PATCH 295/496] In zip namespace fixtures, explicitly generate the directory entries implied by children. Workaround for python/cpython#59110. --- importlib_resources/tests/zip.py | 3 +++ setup.cfg | 1 + 2 files changed, 4 insertions(+) diff --git a/importlib_resources/tests/zip.py b/importlib_resources/tests/zip.py index 6ab914b0..962195a9 100755 --- a/importlib_resources/tests/zip.py +++ b/importlib_resources/tests/zip.py @@ -7,6 +7,8 @@ import pathlib import zipfile +import zipp + def make_zip_file(src, dst): """ @@ -16,6 +18,7 @@ def make_zip_file(src, dst): for src_path, rel in walk(src): dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) zf.write(src_path, dst_name) + zipp.CompleteDirs.inject(zf) return dst diff --git a/setup.cfg b/setup.cfg index 52ec4c25..571356dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ testing = pytest-ruff # local + zipp >= 3.17 docs = # upstream From 1d91410d0d709718d9f77fc729f9d3738c6fda1f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 12 Sep 2023 15:42:35 -0400 Subject: [PATCH 296/496] Add xfail tests for namespace packages in a zip, capturing missed expectation reported in python/importlib_resources#287. --- importlib_resources/tests/test_resource.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 40368056..850d0294 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -3,6 +3,8 @@ import importlib_resources as resources import pathlib +import pytest + from . import data01 from . import util from importlib import import_module @@ -209,5 +211,14 @@ def tearDownClass(cls): sys.path.remove(cls.site_dir) +@pytest.mark.xfail +class ResourceFromNamespaceZipTests( + util.ZipSetupBase, + ResourceFromNamespaceTests, + unittest.TestCase, +): + ZIP_MODULE = 'namespacedata01' + + if __name__ == '__main__': unittest.main() From c02bc7ed1e1b1468cf0ba781ab01ab08916f1d2f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2023 09:43:24 -0400 Subject: [PATCH 297/496] Update MultiplexedPath to expect Traversable and add a compatibility shim with deprecation warning. --- importlib_resources/_compat.py | 17 +++++++++++++++++ importlib_resources/readers.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index a93a8826..d7e9f0d9 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -4,6 +4,7 @@ import os import sys import pathlib +import warnings from contextlib import suppress from typing import Union @@ -107,3 +108,19 @@ def wrap_spec(package): else: # PathLike is only subscriptable at runtime in 3.9+ StrPath = Union[str, "os.PathLike[str]"] + + +def ensure_traversable(path): + """ + Convert deprecated string arguments to traversables (pathlib.Path). + """ + if not isinstance(path, str): + return path + + warnings.warn( + "String arguments are deprecated. Pass a Traversable instead.", + DeprecationWarning, + stacklevel=3, + ) + + return pathlib.Path(path) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 51d030a6..dc1ec941 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -6,7 +6,7 @@ from . import abc from ._itertools import only -from ._compat import ZipPath +from ._compat import ZipPath, ensure_traversable def remove_duplicates(items): @@ -62,7 +62,7 @@ class MultiplexedPath(abc.Traversable): """ def __init__(self, *paths): - self._paths = list(map(pathlib.Path, remove_duplicates(paths))) + self._paths = list(map(ensure_traversable, remove_duplicates(paths))) if not self._paths: message = 'MultiplexedPath must contain at least one path' raise FileNotFoundError(message) From f004f042e9a418e1f4bc94b9cd20d426b7c6d74e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2023 09:45:16 -0400 Subject: [PATCH 298/496] Update tests for MultiplexedPath to pass traversables, addressing some deprecation warnings. --- importlib_resources/tests/test_reader.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index e2bdf19c..a1eadb2c 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -10,8 +10,7 @@ class MultiplexedPathTest(unittest.TestCase): @classmethod def setUpClass(cls): - path = pathlib.Path(__file__).parent / 'namespacedata01' - cls.folder = str(path) + cls.folder = pathlib.Path(__file__).parent / 'namespacedata01' def test_init_no_paths(self): with self.assertRaises(FileNotFoundError): @@ -19,7 +18,7 @@ def test_init_no_paths(self): def test_init_file(self): with self.assertRaises(NotADirectoryError): - MultiplexedPath(os.path.join(self.folder, 'binary.file')) + MultiplexedPath(self.folder / 'binary.file') def test_iterdir(self): contents = {path.name for path in MultiplexedPath(self.folder).iterdir()} @@ -30,7 +29,7 @@ def test_iterdir(self): self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'}) def test_iterdir_duplicate(self): - data01 = os.path.abspath(os.path.join(__file__, '..', 'data01')) + data01 = pathlib.Path(__file__).parent.joinpath('data01') contents = { path.name for path in MultiplexedPath(self.folder, data01).iterdir() } @@ -60,8 +59,8 @@ def test_open_file(self): path.open() def test_join_path(self): - prefix = os.path.abspath(os.path.join(__file__, '..')) - data01 = os.path.join(prefix, 'data01') + data01 = pathlib.Path(__file__).parent.joinpath('data01') + prefix = str(data01.parent) path = MultiplexedPath(self.folder, data01) self.assertEqual( str(path.joinpath('binary.file'))[len(prefix) + 1 :], @@ -82,9 +81,9 @@ def test_join_path_compound(self): assert not path.joinpath('imaginary/foo.py').exists() def test_join_path_common_subdir(self): - prefix = os.path.abspath(os.path.join(__file__, '..')) - data01 = os.path.join(prefix, 'data01') - data02 = os.path.join(prefix, 'data02') + data01 = pathlib.Path(__file__).parent.joinpath('data01') + data02 = pathlib.Path(__file__).parent.joinpath('data02') + prefix = str(data01.parent) path = MultiplexedPath(data01, data02) self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) self.assertEqual( From f23b743f45fe75b6b10d004fd3ce69d181849ae4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2023 10:02:24 -0400 Subject: [PATCH 299/496] Update changelog --- newsfragments/+13deb64a.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+13deb64a.feature.rst diff --git a/newsfragments/+13deb64a.feature.rst b/newsfragments/+13deb64a.feature.rst new file mode 100644 index 00000000..a765911e --- /dev/null +++ b/newsfragments/+13deb64a.feature.rst @@ -0,0 +1 @@ +MultiplexedPath now expects Traversable paths. String arguments to MultiplexedPath are now deprecated. \ No newline at end of file From 463331b37e9811d4f231d2b70d17df7cea7a6551 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Sep 2023 09:16:01 -0400 Subject: [PATCH 300/496] When constructing a MultiplexedPath, resolve submodule_search_locations to Traversable objects. Closes python/importlib_resources#287. --- importlib_resources/readers.py | 28 +++++++++++++++++++++- importlib_resources/tests/test_resource.py | 3 --- newsfragments/287.bugfix.rst | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 newsfragments/287.bugfix.rst diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index dc1ec941..6a45a23f 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,7 +1,9 @@ import collections +import contextlib import itertools import pathlib import operator +import re from . import abc @@ -130,7 +132,31 @@ class NamespaceReader(abc.TraversableResources): def __init__(self, namespace_path): if 'NamespacePath' not in str(namespace_path): raise ValueError('Invalid path') - self.path = MultiplexedPath(*list(namespace_path)) + self.path = MultiplexedPath(*map(self._resolve, namespace_path)) + + @classmethod + def _resolve(cls, path_str) -> abc.Traversable: + """ + Given an item from a namespace path, resolve it to a Traversable. + + path_str might be a directory on the filesystem or a path to a + zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or + ``/foo/baz.zip/inner_dir``. + """ + (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) + return dir + + @classmethod + def _candidate_paths(cls, path_str): + yield pathlib.Path(path_str) + yield from cls._resolve_zip_path(path_str) + + @staticmethod + def _resolve_zip_path(path_str): + for match in reversed(list(re.finditer('/', path_str))): + with contextlib.suppress(FileNotFoundError, IsADirectoryError): + inner = path_str[match.end() :] + yield ZipPath(path_str[: match.start()], inner + '/' * len(inner)) def resource_path(self, resource): """ diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 850d0294..de7d734b 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -3,8 +3,6 @@ import importlib_resources as resources import pathlib -import pytest - from . import data01 from . import util from importlib import import_module @@ -211,7 +209,6 @@ def tearDownClass(cls): sys.path.remove(cls.site_dir) -@pytest.mark.xfail class ResourceFromNamespaceZipTests( util.ZipSetupBase, ResourceFromNamespaceTests, diff --git a/newsfragments/287.bugfix.rst b/newsfragments/287.bugfix.rst new file mode 100644 index 00000000..9ef3c0b4 --- /dev/null +++ b/newsfragments/287.bugfix.rst @@ -0,0 +1 @@ +Enabled support for resources in namespace packages in zip files. From a9b0c92b303ba34a1631ce2f60c9ee16ea062d71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Sep 2023 11:33:00 -0400 Subject: [PATCH 301/496] Honor backslashes in inner paths as found in submodule_search_locations. --- importlib_resources/readers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 6a45a23f..1e2d1bac 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -136,12 +136,12 @@ def __init__(self, namespace_path): @classmethod def _resolve(cls, path_str) -> abc.Traversable: - """ + r""" Given an item from a namespace path, resolve it to a Traversable. path_str might be a directory on the filesystem or a path to a zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or - ``/foo/baz.zip/inner_dir``. + ``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. """ (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) return dir @@ -153,10 +153,12 @@ def _candidate_paths(cls, path_str): @staticmethod def _resolve_zip_path(path_str): - for match in reversed(list(re.finditer('/', path_str))): - with contextlib.suppress(FileNotFoundError, IsADirectoryError): - inner = path_str[match.end() :] - yield ZipPath(path_str[: match.start()], inner + '/' * len(inner)) + for match in reversed(list(re.finditer(r'[\\/]', path_str))): + with contextlib.suppress( + FileNotFoundError, IsADirectoryError, PermissionError + ): + inner = path_str[match.end() :].replace('\\', '/') + '/' + yield ZipPath(path_str[: match.start()], inner.lstrip('/')) def resource_path(self, resource): """ From babc28763cd8e4f6fc4d6d67136ff51fb9182069 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Sep 2023 14:00:30 -0400 Subject: [PATCH 302/496] Finalize --- NEWS.rst | 15 +++++++++++++++ newsfragments/+13deb64a.feature.rst | 1 - newsfragments/287.bugfix.rst | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+13deb64a.feature.rst delete mode 100644 newsfragments/287.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 5c97c82d..83126c2d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,18 @@ +v6.1.0 +====== + +Features +-------- + +- MultiplexedPath now expects Traversable paths. String arguments to MultiplexedPath are now deprecated. + + +Bugfixes +-------- + +- Enabled support for resources in namespace packages in zip files. (#287) + + v6.0.1 ====== diff --git a/newsfragments/+13deb64a.feature.rst b/newsfragments/+13deb64a.feature.rst deleted file mode 100644 index a765911e..00000000 --- a/newsfragments/+13deb64a.feature.rst +++ /dev/null @@ -1 +0,0 @@ -MultiplexedPath now expects Traversable paths. String arguments to MultiplexedPath are now deprecated. \ No newline at end of file diff --git a/newsfragments/287.bugfix.rst b/newsfragments/287.bugfix.rst deleted file mode 100644 index 9ef3c0b4..00000000 --- a/newsfragments/287.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Enabled support for resources in namespace packages in zip files. From 03f03e7802b0842b41f70b2b1c17ab26551a7533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:43:46 -0500 Subject: [PATCH 303/496] 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 1496c81842b13edfb9b7511e1f56f4cb7da66f87 Mon Sep 17 00:00:00 2001 From: Alexander Shadchin Date: Mon, 6 Nov 2023 14:51:49 +0300 Subject: [PATCH 304/496] Add missed `stream` argument --- importlib_resources/simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index 7770c922..96f117fe 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -88,7 +88,7 @@ def is_dir(self): def open(self, mode='r', *args, **kwargs): stream = self.parent.reader.open_binary(self.name) if 'b' not in mode: - stream = io.TextIOWrapper(*args, **kwargs) + stream = io.TextIOWrapper(stream, *args, **kwargs) return stream def joinpath(self, name): From d3c726360526661da6ba8523598ab4e2c3a710b9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 6 Nov 2023 19:41:08 -0500 Subject: [PATCH 305/496] Add news fragment. --- newsfragments/+c8cc7ec5.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+c8cc7ec5.bugfix.rst diff --git a/newsfragments/+c8cc7ec5.bugfix.rst b/newsfragments/+c8cc7ec5.bugfix.rst new file mode 100644 index 00000000..e93d957d --- /dev/null +++ b/newsfragments/+c8cc7ec5.bugfix.rst @@ -0,0 +1 @@ +Added missed stream argument in simple.ResourceHandle. Ref python/cpython#111775. \ No newline at end of file From f625a8ab82523d595604145a90be8d58dfb57c2c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 6 Nov 2023 19:42:12 -0500 Subject: [PATCH 306/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+c8cc7ec5.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+c8cc7ec5.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 83126c2d..eaf50717 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.1.1 +====== + +Bugfixes +-------- + +- Added missed stream argument in simple.ResourceHandle. Ref python/cpython#111775. + + v6.1.0 ====== diff --git a/newsfragments/+c8cc7ec5.bugfix.rst b/newsfragments/+c8cc7ec5.bugfix.rst deleted file mode 100644 index e93d957d..00000000 --- a/newsfragments/+c8cc7ec5.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Added missed stream argument in simple.ResourceHandle. Ref python/cpython#111775. \ No newline at end of file From 75d9cc1b7cb6f84e7a16a83ec3abb9a478fdb130 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 19:57:45 +0600 Subject: [PATCH 307/496] 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 308/496] 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 309/496] 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 310/496] 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 97a5f44787ac5a928534cdf724210c429621435c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:53:37 -0500 Subject: [PATCH 311/496] 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 312/496] 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 313/496] 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 9a7fccf146f13ea02b4c67d591a913909f1dadd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 18:07:31 -0500 Subject: [PATCH 314/496] Update test_custom_loader to correctly return a Traversable (pathlib.Path) and adjust the assertions to only expect a Traversable. --- importlib_resources/tests/test_custom.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_resources/tests/test_custom.py b/importlib_resources/tests/test_custom.py index e85ddd65..35336f8f 100644 --- a/importlib_resources/tests/test_custom.py +++ b/importlib_resources/tests/test_custom.py @@ -3,6 +3,7 @@ import pathlib import importlib_resources as resources +from .. import abc from ..abc import TraversableResources, ResourceReader from . import util from ._compat import os_helper @@ -38,8 +39,9 @@ def setUp(self): self.addCleanup(self.fixtures.close) def test_custom_loader(self): - temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + temp_dir = pathlib.Path(self.fixtures.enter_context(os_helper.temp_dir())) loader = SimpleLoader(MagicResources(temp_dir)) pkg = util.create_package_from_loader(loader) files = resources.files(pkg) - assert files is temp_dir + assert isinstance(files, abc.Traversable) + assert list(files.iterdir()) == [] From a95bafd7153e2b392ef15d50a6a29f10f91ba6f0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 18:23:11 -0500 Subject: [PATCH 315/496] Remove skip as typing now has runtime_checkable unconditionally. --- importlib_resources/tests/test_files.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 197a063b..36267e9d 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,4 +1,3 @@ -import typing import textwrap import unittest import warnings @@ -31,10 +30,6 @@ def test_read_text(self): actual = files.joinpath('utf-8.file').read_text(encoding='utf-8') assert actual == 'Hello, UTF-8 world!\n' - @unittest.skipUnless( - hasattr(typing, 'runtime_checkable'), - "Only suitable when typing supports runtime_checkable", - ) def test_traversable(self): assert isinstance(resources.files(self.data), Traversable) From 80fe9cc7f91d580942ee671d883226706989161d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 18:27:41 -0500 Subject: [PATCH 316/496] Add note about when to drop compatibility. --- importlib_resources/_common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 3c6de1cf..b591cce5 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -27,6 +27,8 @@ def package_to_anchor(func): >>> files('a', 'b') Traceback (most recent call last): TypeError: files() takes from 0 to 1 positional arguments but 2 were given + + Remove this compatibility in Python 3.14. """ undefined = object() From 376653975e55e34c7df438d5704668dec6968319 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 18:36:28 -0500 Subject: [PATCH 317/496] Replace compatibility code. --- importlib_resources/tests/test_path.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index 7cb20003..f8c045f6 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -23,10 +23,7 @@ def test_reading(self): target = resources.files(self.data) / 'utf-8.file' with resources.as_file(target) as path: self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) - # pathlib.Path.read_text() was introduced in Python 3.5. - with path.open('r', encoding='utf-8') as file: - text = file.read() - self.assertEqual('Hello, UTF-8 world!\n', text) + self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) class PathDiskTests(PathTests, unittest.TestCase): From 63eb1caf4f1cca60abc71b9ea59a3fb216695b73 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 18:37:42 -0500 Subject: [PATCH 318/496] Make the assertion explicit. --- importlib_resources/tests/test_path.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index f8c045f6..c3e1cbb4 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -1,4 +1,5 @@ import io +import pathlib import unittest import importlib_resources as resources @@ -15,13 +16,11 @@ def execute(self, package, path): class PathTests: def test_reading(self): """ - Path should be readable. - - Test also implicitly verifies the returned object is a pathlib.Path - instance. + Path should be readable and a pathlib.Path instance. """ target = resources.files(self.data) / 'utf-8.file' with resources.as_file(target) as path: + self.assertIsInstance(path, pathlib.Path) self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) From 596e6834c8a037c935338afe92e0b9c5ffa1768f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Dec 2023 18:29:16 -0500 Subject: [PATCH 319/496] 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 320/496] 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 321/496] 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 322/496] =?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 323/496] 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 324/496] 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 325/496] 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 326/496] 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 29e5d34af962e59e92c501ebb988dcaf192b114e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Feb 2024 10:15:04 -0500 Subject: [PATCH 327/496] 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 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 328/496] 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 329/496] 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 330/496] 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 331/496] 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 56915b8b2b8e112e31219f4b639e4b733e4c49ba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 10:11:42 -0500 Subject: [PATCH 332/496] =?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 --- docs/conf.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b4eaace9..c9b47e44 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,9 +48,7 @@ extensions += ['jaraco.tidelift'] -nitpick_ignore.extend( - [ - ('py:class', 'module'), - ('py:class', '_io.BufferedReader'), - ] -) +nitpick_ignore.extend([ + ('py:class', 'module'), + ('py:class', '_io.BufferedReader'), +]) From 3cf884a25248925533e116c3a7a02417a36193dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 12:41:45 -0500 Subject: [PATCH 333/496] Move '_ensure_traversable' to the readers module, as it's needed downstream. --- importlib_resources/_compat.py | 16 ---------------- importlib_resources/readers.py | 23 +++++++++++++++++++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index d7e9f0d9..519ccfa4 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -108,19 +108,3 @@ def wrap_spec(package): else: # PathLike is only subscriptable at runtime in 3.9+ StrPath = Union[str, "os.PathLike[str]"] - - -def ensure_traversable(path): - """ - Convert deprecated string arguments to traversables (pathlib.Path). - """ - if not isinstance(path, str): - return path - - warnings.warn( - "String arguments are deprecated. Pass a Traversable instead.", - DeprecationWarning, - stacklevel=3, - ) - - return pathlib.Path(path) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 1e2d1bac..ef2d21ad 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -4,11 +4,12 @@ import pathlib import operator import re +import warnings from . import abc from ._itertools import only -from ._compat import ZipPath, ensure_traversable +from ._compat import ZipPath def remove_duplicates(items): @@ -64,7 +65,7 @@ class MultiplexedPath(abc.Traversable): """ def __init__(self, *paths): - self._paths = list(map(ensure_traversable, remove_duplicates(paths))) + self._paths = list(map(_ensure_traversable, remove_duplicates(paths))) if not self._paths: message = 'MultiplexedPath must contain at least one path' raise FileNotFoundError(message) @@ -170,3 +171,21 @@ def resource_path(self, resource): def files(self): return self.path + + +def _ensure_traversable(path): + """ + Convert deprecated string arguments to traversables (pathlib.Path). + + Remove with Python 3.15. + """ + if not isinstance(path, str): + return path + + warnings.warn( + "String arguments are deprecated. Pass a Traversable instead.", + DeprecationWarning, + stacklevel=3, + ) + + return pathlib.Path(path) From be65d3dc2cd3e156920f4ccfb8ec3eb34bcb3a74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 12:33:33 -0500 Subject: [PATCH 334/496] Move compatibility and future behaviors into separate packages. --- importlib_resources/_common.py | 2 +- importlib_resources/abc.py | 3 +- importlib_resources/compat/__init__.py | 0 importlib_resources/compat/py38.py | 11 +++++++ importlib_resources/compat/py39.py | 10 ++++++ importlib_resources/future/__init__.py | 0 .../{_compat.py => future/adapters.py} | 31 +------------------ importlib_resources/readers.py | 2 +- 8 files changed, 26 insertions(+), 33 deletions(-) create mode 100644 importlib_resources/compat/__init__.py create mode 100644 importlib_resources/compat/py38.py create mode 100644 importlib_resources/compat/py39.py create mode 100644 importlib_resources/future/__init__.py rename importlib_resources/{_compat.py => future/adapters.py} (77%) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index b591cce5..c9891359 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -12,7 +12,7 @@ from typing import Union, Optional, cast from .abc import ResourceReader, Traversable -from ._compat import wrap_spec +from .future.adapters import wrap_spec Package = Union[types.ModuleType, str] Anchor = Package diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 23b6aeaf..7a58dd2f 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -3,8 +3,9 @@ import itertools import pathlib from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional +from typing import runtime_checkable, Protocol -from ._compat import runtime_checkable, Protocol, StrPath +from .compat.py38 import StrPath __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/importlib_resources/compat/__init__.py b/importlib_resources/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_resources/compat/py38.py b/importlib_resources/compat/py38.py new file mode 100644 index 00000000..4d548257 --- /dev/null +++ b/importlib_resources/compat/py38.py @@ -0,0 +1,11 @@ +import os +import sys + +from typing import Union + + +if sys.version_info >= (3, 9): + StrPath = Union[str, os.PathLike[str]] +else: + # PathLike is only subscriptable at runtime in 3.9+ + StrPath = Union[str, "os.PathLike[str]"] diff --git a/importlib_resources/compat/py39.py b/importlib_resources/compat/py39.py new file mode 100644 index 00000000..ab87b9dc --- /dev/null +++ b/importlib_resources/compat/py39.py @@ -0,0 +1,10 @@ +import sys + + +__all__ = ['ZipPath'] + + +if sys.version_info >= (3, 10): + from zipfile import Path as ZipPath # type: ignore +else: + from zipp import Path as ZipPath # type: ignore diff --git a/importlib_resources/future/__init__.py b/importlib_resources/future/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_resources/_compat.py b/importlib_resources/future/adapters.py similarity index 77% rename from importlib_resources/_compat.py rename to importlib_resources/future/adapters.py index 519ccfa4..1f0cad12 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/future/adapters.py @@ -8,25 +8,7 @@ from contextlib import suppress from typing import Union - -if sys.version_info >= (3, 10): - from zipfile import Path as ZipPath # type: ignore -else: - from zipp import Path as ZipPath # type: ignore - - -try: - from typing import runtime_checkable # type: ignore -except ImportError: - - def runtime_checkable(cls): # type: ignore - return cls - - -try: - from typing import Protocol # type: ignore -except ImportError: - Protocol = abc.ABC # type: ignore +from .. import readers, _adapters class TraversableResourcesLoader: @@ -46,8 +28,6 @@ def path(self): return self.spec.origin def get_resource_reader(self, name): - from . import readers, _adapters - def _zip_reader(spec): with suppress(AttributeError): return readers.ZipReader(spec.loader, spec.name) @@ -98,13 +78,4 @@ def wrap_spec(package): Supersedes _adapters.wrap_spec to use TraversableResourcesLoader from above for older Python compatibility (<3.10). """ - from . import _adapters - return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) - - -if sys.version_info >= (3, 9): - StrPath = Union[str, os.PathLike[str]] -else: - # PathLike is only subscriptable at runtime in 3.9+ - StrPath = Union[str, "os.PathLike[str]"] diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index ef2d21ad..cad10e53 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -9,7 +9,7 @@ from . import abc from ._itertools import only -from ._compat import ZipPath +from .compat.py39 import ZipPath def remove_duplicates(items): From 8b10d7ecb697c3ceb666b22e1e066c968ec66f35 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 12:51:55 -0500 Subject: [PATCH 335/496] Consolidate more shared behavior across TraversableResourceLoaders. --- importlib_resources/future/adapters.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 1f0cad12..cd5067dc 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -11,7 +11,7 @@ from .. import readers, _adapters -class TraversableResourcesLoader: +class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ Adapt loaders to provide TraversableResources and other compatibility. @@ -20,9 +20,6 @@ class TraversableResourcesLoader: loaders do not yet implement TraversableResources. """ - def __init__(self, spec): - self.spec = spec - @property def path(self): return self.spec.origin @@ -36,14 +33,6 @@ def _namespace_reader(spec): with suppress(AttributeError, ValueError): return readers.NamespaceReader(spec.submodule_search_locations) - def _available_reader(spec): - with suppress(AttributeError): - return spec.loader.get_resource_reader(spec.name) - - def _native_reader(spec): - reader = _available_reader(spec) - return reader if hasattr(reader, 'files') else None - def _file_reader(spec): try: path = pathlib.Path(self.path) @@ -62,11 +51,8 @@ def _file_reader(spec): # local FileReader _file_reader(self.spec) or - # native reader if it supplies 'files' - _native_reader(self.spec) - or - # fallback - adapt the spec ResourceReader to TraversableReader - _adapters.CompatibilityFiles(self.spec) + # fallback + super().get_resource_reader(name) ) From beb50118fdc76e396deb0b6ca1d30a3339844979 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 13:00:08 -0500 Subject: [PATCH 336/496] Update comments to reflect refreshed purpose. --- importlib_resources/future/adapters.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index cd5067dc..d787f1b4 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -16,8 +16,8 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): Adapt loaders to provide TraversableResources and other compatibility. - Used primarily for Python 3.9 and earlier where the native - loaders do not yet implement TraversableResources. + Ensures the readers from importlib_resources are preferred + over stdlib readers. """ @property @@ -58,10 +58,8 @@ def _file_reader(spec): def wrap_spec(package): """ - Construct a package spec with traversable compatibility - on the spec/loader/reader. - - Supersedes _adapters.wrap_spec to use TraversableResourcesLoader - from above for older Python compatibility (<3.10). + Override _adapters.wrap_spec to use TraversableResourcesLoader + from above. Ensures that future behavior is always available on older + Pythons. """ return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) From 115ca5dee0bdecff43766be789f34979efb8d788 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 13:22:04 -0500 Subject: [PATCH 337/496] Move reader constructors into methods. --- importlib_resources/future/adapters.py | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index d787f1b4..1983edb2 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -20,41 +20,41 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): over stdlib readers. """ - @property - def path(self): - return self.spec.origin - def get_resource_reader(self, name): - def _zip_reader(spec): - with suppress(AttributeError): - return readers.ZipReader(spec.loader, spec.name) - - def _namespace_reader(spec): - with suppress(AttributeError, ValueError): - return readers.NamespaceReader(spec.submodule_search_locations) - - def _file_reader(spec): - try: - path = pathlib.Path(self.path) - except TypeError: - return None - if path.exists(): - return readers.FileReader(self) - return ( # local ZipReader if a zip module - _zip_reader(self.spec) + self._zip_reader() or # local NamespaceReader if a namespace module - _namespace_reader(self.spec) + self._namespace_reader() or # local FileReader - _file_reader(self.spec) + self._file_reader() or # fallback super().get_resource_reader(name) ) + @property + def path(self): + return self.spec.origin + + def _zip_reader(self): + with suppress(AttributeError): + return readers.ZipReader(self.spec.loader, self.spec.name) + + def _namespace_reader(self): + with suppress(AttributeError, ValueError): + return readers.NamespaceReader(self.spec.submodule_search_locations) + + def _file_reader(self): + try: + path = pathlib.Path(self.path) + except TypeError: + return None + if path.exists(): + return readers.FileReader(self) + def wrap_spec(package): """ From 321797927c8460c1e9432863a4244cbb7285a908 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 13:23:26 -0500 Subject: [PATCH 338/496] Use a SimpleNamespace to pass the path needed by the FileReader. --- importlib_resources/future/adapters.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 1983edb2..63e3b991 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -6,6 +6,7 @@ import pathlib import warnings from contextlib import suppress +from types import SimpleNamespace from typing import Union from .. import readers, _adapters @@ -35,10 +36,6 @@ def get_resource_reader(self, name): super().get_resource_reader(name) ) - @property - def path(self): - return self.spec.origin - def _zip_reader(self): with suppress(AttributeError): return readers.ZipReader(self.spec.loader, self.spec.name) @@ -49,11 +46,11 @@ def _namespace_reader(self): def _file_reader(self): try: - path = pathlib.Path(self.path) + path = pathlib.Path(self.spec.origin) except TypeError: return None if path.exists(): - return readers.FileReader(self) + return readers.FileReader(SimpleNamespace(path=path)) def wrap_spec(package): From 12c3c6296f00610d7ecd69fb633719eafb85858f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 13:27:04 -0500 Subject: [PATCH 339/496] Extract method for the standard readers. --- importlib_resources/future/adapters.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 63e3b991..2d67c35c 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -22,19 +22,10 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ def get_resource_reader(self, name): - return ( - # local ZipReader if a zip module - self._zip_reader() - or - # local NamespaceReader if a namespace module - self._namespace_reader() - or - # local FileReader - self._file_reader() - or - # fallback - super().get_resource_reader(name) - ) + return self._standard_reader() or super().get_resource_reader(name) + + def _standard_reader(self): + return self._zip_reader() or self._namespace_reader() or self._file_reader() def _zip_reader(self): with suppress(AttributeError): From 37368450def57ab7877e815a30f58869a04ee36f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 13:28:04 -0500 Subject: [PATCH 340/496] Remove flake8 exclusion and remove unused imports. --- importlib_resources/future/adapters.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 2d67c35c..56dea7cf 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -1,13 +1,6 @@ -# flake8: noqa - -import abc -import os -import sys import pathlib -import warnings from contextlib import suppress from types import SimpleNamespace -from typing import Union from .. import readers, _adapters From d93e241473a834f8741464ee4b1d945f390b1721 Mon Sep 17 00:00:00 2001 From: Weicheng Zhao Date: Tue, 2 Jan 2024 17:04:01 -0800 Subject: [PATCH 341/496] Fix NotADirectory error when calling files on a submodule of a zipped namespace package --- importlib_resources/readers.py | 2 +- .../namespacedata01/subdirectory/binary.file | Bin 0 -> 4 bytes importlib_resources/tests/test_contents.py | 2 +- importlib_resources/tests/test_files.py | 4 ++++ importlib_resources/tests/test_open.py | 4 ++++ importlib_resources/tests/test_read.py | 15 +++++++++++++ importlib_resources/tests/test_reader.py | 8 +++++-- importlib_resources/tests/test_resource.py | 20 ++++++++++++++++-- 8 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 importlib_resources/tests/namespacedata01/subdirectory/binary.file diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index cad10e53..29daafea 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -156,7 +156,7 @@ def _candidate_paths(cls, path_str): def _resolve_zip_path(path_str): for match in reversed(list(re.finditer(r'[\\/]', path_str))): with contextlib.suppress( - FileNotFoundError, IsADirectoryError, PermissionError + FileNotFoundError, IsADirectoryError, NotADirectoryError, PermissionError ): inner = path_str[match.end() :].replace('\\', '/') + '/' yield ZipPath(path_str[: match.start()], inner.lstrip('/')) diff --git a/importlib_resources/tests/namespacedata01/subdirectory/binary.file b/importlib_resources/tests/namespacedata01/subdirectory/binary.file new file mode 100644 index 0000000000000000000000000000000000000000..eaf36c1daccfdf325514461cd1a2ffbc139b5464 GIT binary patch literal 4 LcmZQzWMT#Y01f~L literal 0 HcmV?d00001 diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 525568e8..7dc3b0a6 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -31,8 +31,8 @@ class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): class ContentsNamespaceTests(ContentsTests, unittest.TestCase): expected = { # no __init__ because of namespace design - # no subdirectory as incidental difference in fixture 'binary.file', + 'subdirectory', 'utf-16.file', 'utf-8.file', } diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 36267e9d..51ecc896 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -58,6 +58,10 @@ def setUp(self): self.data = namespacedata01 +class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + class SiteDir: def setUp(self): self.fixtures = contextlib.ExitStack() diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 83b737dc..bb75d876 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -81,5 +81,9 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): pass +class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + if __name__ == '__main__': unittest.main() diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 5b83221e..ef009801 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -76,5 +76,20 @@ def setUp(self): self.data = namespacedata01 +class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + def test_read_submodule_resource(self): + submodule = import_module('namespacedata01.subdirectory') + result = resources.files(submodule).joinpath('binary.file').read_bytes() + self.assertEqual(result, b'\0\1\2\3') + + def test_read_submodule_resource_by_name(self): + result = ( + resources.files('namespacedata01.subdirectory').joinpath('binary.file').read_bytes() + ) + self.assertEqual(result, b'\0\1\2\3') + + if __name__ == '__main__': unittest.main() diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index a1eadb2c..49d621fb 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -26,7 +26,7 @@ def test_iterdir(self): contents.remove('__pycache__') except (KeyError, ValueError): pass - self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'}) + self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}) def test_iterdir_duplicate(self): data01 = pathlib.Path(__file__).parent.joinpath('data01') @@ -67,7 +67,11 @@ def test_join_path(self): os.path.join('namespacedata01', 'binary.file'), ) self.assertEqual( - str(path.joinpath('subdirectory'))[len(prefix) + 1 :], + str(path.joinpath('subdirectory')._paths[0])[len(prefix) + 1 :], + os.path.join('namespacedata01', 'subdirectory'), + ) + self.assertEqual( + str(path.joinpath('subdirectory')._paths[1])[len(prefix) + 1 :], os.path.join('data01', 'subdirectory'), ) self.assertEqual( diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index de7d734b..4f13ad88 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -186,7 +186,7 @@ def test_submodule_contents(self): contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}) def test_submodule_contents_by_name(self): contents = names(resources.files('namespacedata01')) @@ -194,7 +194,23 @@ def test_submodule_contents_by_name(self): contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}) + + def test_submodule_sub_contents(self): + contents = names(resources.files(import_module('namespacedata01.subdirectory'))) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file'}) + + def test_submodule_sub_contents_by_name(self): + contents = names(resources.files('namespacedata01.subdirectory')) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file'}) class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase): From b68509f05db4881fce2f053122daf7bd47d61ad9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 14:25:54 -0500 Subject: [PATCH 342/496] =?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 --- importlib_resources/readers.py | 5 ++++- importlib_resources/tests/test_read.py | 4 +++- importlib_resources/tests/test_reader.py | 4 +++- importlib_resources/tests/test_resource.py | 8 ++++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 29daafea..4a80a774 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -156,7 +156,10 @@ def _candidate_paths(cls, path_str): def _resolve_zip_path(path_str): for match in reversed(list(re.finditer(r'[\\/]', path_str))): with contextlib.suppress( - FileNotFoundError, IsADirectoryError, NotADirectoryError, PermissionError + FileNotFoundError, + IsADirectoryError, + NotADirectoryError, + PermissionError, ): inner = path_str[match.end() :].replace('\\', '/') + '/' yield ZipPath(path_str[: match.start()], inner.lstrip('/')) diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index ef009801..b0a8c9f2 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -86,7 +86,9 @@ def test_read_submodule_resource(self): def test_read_submodule_resource_by_name(self): result = ( - resources.files('namespacedata01.subdirectory').joinpath('binary.file').read_bytes() + resources.files('namespacedata01.subdirectory') + .joinpath('binary.file') + .read_bytes() ) self.assertEqual(result, b'\0\1\2\3') diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 49d621fb..77fb2192 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -26,7 +26,9 @@ def test_iterdir(self): contents.remove('__pycache__') except (KeyError, ValueError): pass - self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'}) + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'} + ) def test_iterdir_duplicate(self): data01 = pathlib.Path(__file__).parent.joinpath('data01') diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 4f13ad88..dc2a108c 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -186,7 +186,9 @@ def test_submodule_contents(self): contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}) + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} + ) def test_submodule_contents_by_name(self): contents = names(resources.files('namespacedata01')) @@ -194,7 +196,9 @@ def test_submodule_contents_by_name(self): contents.remove('__pycache__') except KeyError: pass - self.assertEqual(contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'}) + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} + ) def test_submodule_sub_contents(self): contents = names(resources.files(import_module('namespacedata01.subdirectory'))) From 6c9b19006cbedd5237ee37bc1cdfd41d85ef0d30 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 15:10:50 -0500 Subject: [PATCH 343/496] Change the assertions for test_join_path to assert that the subdirectory is a multiplexed path to avoid touching implementation details. --- importlib_resources/tests/test_reader.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 77fb2192..95c2fc85 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -68,14 +68,10 @@ def test_join_path(self): str(path.joinpath('binary.file'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'binary.file'), ) - self.assertEqual( - str(path.joinpath('subdirectory')._paths[0])[len(prefix) + 1 :], - os.path.join('namespacedata01', 'subdirectory'), - ) - self.assertEqual( - str(path.joinpath('subdirectory')._paths[1])[len(prefix) + 1 :], - os.path.join('data01', 'subdirectory'), - ) + sub = path.joinpath('subdirectory') + assert isinstance(sub, MultiplexedPath) + assert 'namespacedata01' in str(sub) + assert 'data01' in str(sub) self.assertEqual( str(path.joinpath('imaginary'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'imaginary'), From 168f6179745e0563deaf5d4f9b1e9c89c0dbe3fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 15:24:38 -0500 Subject: [PATCH 344/496] Updated the contents of the binary files in the subdirectories so there's less chance of accidentally matching one for the other. --- .../tests/data01/subdirectory/binary.file | Bin 4 -> 4 bytes .../tests/namespacedata01/subdirectory/binary.file | Bin 4 -> 4 bytes importlib_resources/tests/test_read.py | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/importlib_resources/tests/data01/subdirectory/binary.file b/importlib_resources/tests/data01/subdirectory/binary.file index eaf36c1daccfdf325514461cd1a2ffbc139b5464..5bd8bb897b13225c93a1d26baa88c96b7bd5d817 100644 GIT binary patch literal 4 LcmZQ!Wn%{b05$*@ literal 4 LcmZQzWMT#Y01f~L diff --git a/importlib_resources/tests/namespacedata01/subdirectory/binary.file b/importlib_resources/tests/namespacedata01/subdirectory/binary.file index eaf36c1daccfdf325514461cd1a2ffbc139b5464..100f50643d8d21785f77f83997f2a74c58029a1e 100644 GIT binary patch literal 4 Lcmd<#<>Lnc0EPfJ literal 4 LcmZQzWMT#Y01f~L diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index b0a8c9f2..6c512ce1 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -60,13 +60,13 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('data01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, b'\0\1\2\3') + self.assertEqual(result, b'\4\5\6\x07') def test_read_submodule_resource_by_name(self): result = ( resources.files('data01.subdirectory').joinpath('binary.file').read_bytes() ) - self.assertEqual(result, b'\0\1\2\3') + self.assertEqual(result, b'\4\5\6\x07') class ReadNamespaceTests(ReadTests, unittest.TestCase): @@ -82,7 +82,7 @@ class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('namespacedata01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, b'\0\1\2\3') + self.assertEqual(result, b'\x0c\x0d\x0e\x0f') def test_read_submodule_resource_by_name(self): result = ( @@ -90,7 +90,7 @@ def test_read_submodule_resource_by_name(self): .joinpath('binary.file') .read_bytes() ) - self.assertEqual(result, b'\0\1\2\3') + self.assertEqual(result, b'\x0c\x0d\x0e\x0f') if __name__ == '__main__': From d98c55876873353d64ca8b612cf356a6bdc84149 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 15:26:13 -0500 Subject: [PATCH 345/496] Added changelog --- newsfragments/293.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/293.bugfix.rst diff --git a/newsfragments/293.bugfix.rst b/newsfragments/293.bugfix.rst new file mode 100644 index 00000000..e0ca5ecf --- /dev/null +++ b/newsfragments/293.bugfix.rst @@ -0,0 +1 @@ +Fixed NotADirectoryError when calling files on a subdirectory of a namespace package. \ No newline at end of file From 76145e2922b7ba2103ad41c80868409c5481c5a6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 15:29:58 -0500 Subject: [PATCH 346/496] Use bytes ranges instead of bytes literals. --- importlib_resources/tests/test_open.py | 2 +- importlib_resources/tests/test_read.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index bb75d876..44f1018a 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -24,7 +24,7 @@ def test_open_binary(self): target = resources.files(self.data) / 'binary.file' with target.open('rb') as fp: result = fp.read() - self.assertEqual(result, b'\x00\x01\x02\x03') + self.assertEqual(result, bytes(range(4))) def test_open_text_default_encoding(self): target = resources.files(self.data) / 'utf-8.file' diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 6c512ce1..97d90128 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -19,7 +19,7 @@ def execute(self, package, path): class ReadTests: def test_read_bytes(self): result = resources.files(self.data).joinpath('binary.file').read_bytes() - self.assertEqual(result, b'\0\1\2\3') + self.assertEqual(result, bytes(range(4))) def test_read_text_default_encoding(self): result = ( @@ -60,13 +60,13 @@ class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('data01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, b'\4\5\6\x07') + self.assertEqual(result, bytes(range(4, 8))) def test_read_submodule_resource_by_name(self): result = ( resources.files('data01.subdirectory').joinpath('binary.file').read_bytes() ) - self.assertEqual(result, b'\4\5\6\x07') + self.assertEqual(result, bytes(range(4, 8))) class ReadNamespaceTests(ReadTests, unittest.TestCase): @@ -82,7 +82,7 @@ class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): def test_read_submodule_resource(self): submodule = import_module('namespacedata01.subdirectory') result = resources.files(submodule).joinpath('binary.file').read_bytes() - self.assertEqual(result, b'\x0c\x0d\x0e\x0f') + self.assertEqual(result, bytes(range(12, 16))) def test_read_submodule_resource_by_name(self): result = ( @@ -90,7 +90,7 @@ def test_read_submodule_resource_by_name(self): .joinpath('binary.file') .read_bytes() ) - self.assertEqual(result, b'\x0c\x0d\x0e\x0f') + self.assertEqual(result, bytes(range(12, 16))) if __name__ == '__main__': From 131906a73870c0de86e6817dd54f61b26b33db02 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 15:30:50 -0500 Subject: [PATCH 347/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/293.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/293.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index eaf50717..dbfc2227 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.1.2 +====== + +Bugfixes +-------- + +- Fixed NotADirectoryError when calling files on a subdirectory of a namespace package. (#293) + + v6.1.1 ====== diff --git a/newsfragments/293.bugfix.rst b/newsfragments/293.bugfix.rst deleted file mode 100644 index e0ca5ecf..00000000 --- a/newsfragments/293.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed NotADirectoryError when calling files on a subdirectory of a namespace package. \ No newline at end of file From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 348/496] 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 349/496] 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 8eb207101d80b378abcbe4152a790505fcc8ac7c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 15:33:15 -0500 Subject: [PATCH 350/496] Move test compatibility modules into a compat package. --- importlib_resources/tests/compat/__init__.py | 0 importlib_resources/tests/{_compat.py => compat/py39.py} | 9 ++++++--- importlib_resources/tests/test_custom.py | 2 +- importlib_resources/tests/test_files.py | 2 +- importlib_resources/tests/util.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 importlib_resources/tests/compat/__init__.py rename importlib_resources/tests/{_compat.py => compat/py39.py} (89%) diff --git a/importlib_resources/tests/compat/__init__.py b/importlib_resources/tests/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_resources/tests/_compat.py b/importlib_resources/tests/compat/py39.py similarity index 89% rename from importlib_resources/tests/_compat.py rename to importlib_resources/tests/compat/py39.py index e7bf06dd..946280d1 100644 --- a/importlib_resources/tests/_compat.py +++ b/importlib_resources/tests/compat/py39.py @@ -1,10 +1,14 @@ +""" +Backward-compatability shims to support Python 3.9 and earlier. +""" + import os try: from test.support import import_helper # type: ignore except ImportError: - # Python 3.9 and earlier + class import_helper: # type: ignore from test.support import ( modules_setup, @@ -17,13 +21,12 @@ class import_helper: # type: ignore try: from test.support import os_helper # type: ignore except ImportError: - # Python 3.9 compat + class os_helper: # type:ignore from test.support import temp_dir try: - # Python 3.10 from test.support.os_helper import unlink except ImportError: from test.support import unlink as _unlink diff --git a/importlib_resources/tests/test_custom.py b/importlib_resources/tests/test_custom.py index 35336f8f..86c65676 100644 --- a/importlib_resources/tests/test_custom.py +++ b/importlib_resources/tests/test_custom.py @@ -6,7 +6,7 @@ from .. import abc from ..abc import TraversableResources, ResourceReader from . import util -from ._compat import os_helper +from .compat.py39 import os_helper class SimpleLoader: diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 51ecc896..87290340 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -9,7 +9,7 @@ from . import data01 from . import util from . import _path -from ._compat import os_helper, import_helper +from .compat.py39 import os_helper, import_helper @contextlib.contextmanager diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 066f4113..ee0aa0ad 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -8,7 +8,7 @@ from . import data01 from ..abc import ResourceReader -from ._compat import import_helper, os_helper +from .compat.py39 import import_helper, os_helper from . import zip as zip_ From f3f4b0ab8b50297e33343cc5e51b15dcf08c2412 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 10:58:25 -0500 Subject: [PATCH 351/496] gh-116307: Create a new import helper 'isolated modules' and use that instead of 'Clean Import' to ensure that tests from importlib_resources don't leave modules in sys.modules. --- importlib_resources/tests/compat/py312.py | 18 ++++++++++++++++++ importlib_resources/tests/compat/py39.py | 1 - importlib_resources/tests/test_files.py | 5 +++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 importlib_resources/tests/compat/py312.py diff --git a/importlib_resources/tests/compat/py312.py b/importlib_resources/tests/compat/py312.py new file mode 100644 index 00000000..ea9a58ba --- /dev/null +++ b/importlib_resources/tests/compat/py312.py @@ -0,0 +1,18 @@ +import contextlib + +from .py39 import import_helper + + +@contextlib.contextmanager +def isolated_modules(): + """ + Save modules on entry and cleanup on exit. + """ + (saved,) = import_helper.modules_setup() + try: + yield + finally: + import_helper.modules_cleanup(saved) + + +vars(import_helper).setdefault('isolated_modules', isolated_modules) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index 946280d1..3b4965bc 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -14,7 +14,6 @@ class import_helper: # type: ignore modules_setup, modules_cleanup, DirsOnSysPath, - CleanImport, ) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 87290340..9e45f701 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -9,7 +9,8 @@ from . import data01 from . import util from . import _path -from .compat.py39 import os_helper, import_helper +from .compat.py39 import os_helper +from .compat.py312 import import_helper @contextlib.contextmanager @@ -68,7 +69,7 @@ def setUp(self): self.addCleanup(self.fixtures.close) self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) - self.fixtures.enter_context(import_helper.CleanImport()) + self.fixtures.enter_context(import_helper.isolated_modules()) class ModulesFilesTests(SiteDir, unittest.TestCase): From 414e4c0919b0b15d3546aee94d73835c7b24bdc3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 16:02:18 -0500 Subject: [PATCH 352/496] Use a SimpleNamespace for setdefault compatibility. --- importlib_resources/tests/compat/py39.py | 12 ++++++------ setup.cfg | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index 3b4965bc..a1e1e2d0 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -3,18 +3,18 @@ """ import os +import types + +from jaraco.collections import Projection try: from test.support import import_helper # type: ignore except ImportError: + import test.support - class import_helper: # type: ignore - from test.support import ( - modules_setup, - modules_cleanup, - DirsOnSysPath, - ) + names = 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' + import_helper = types.SimpleNamespace(**Projection(names, vars(test.support))) try: diff --git a/setup.cfg b/setup.cfg index 017cdc48..feef5904 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ testing = # local zipp >= 3.17 + jaraco.collections docs = # upstream From a8892ae076dd24f129e51125f28b9a9abf846e8f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 16:07:41 -0500 Subject: [PATCH 353/496] Extract 'from_test_support' helper. --- importlib_resources/tests/compat/py39.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index a1e1e2d0..93fb699c 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -8,21 +8,27 @@ from jaraco.collections import Projection +def from_test_support(*names): + """ + Return a SimpleNamespace of names from test.support. + """ + import test.support + + return types.SimpleNamespace(**Projection(names, vars(test.support))) + + try: from test.support import import_helper # type: ignore except ImportError: - import test.support - - names = 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' - import_helper = types.SimpleNamespace(**Projection(names, vars(test.support))) + import_helper = from_test_support( + 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' + ) try: from test.support import os_helper # type: ignore except ImportError: - - class os_helper: # type:ignore - from test.support import temp_dir + os_helper = from_test_support('temp_dir') try: From 8918e27bc55dd0a71661b061d6371bd0fd3c49fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 16:11:29 -0500 Subject: [PATCH 354/496] Remove unlink compatibility (unused). --- importlib_resources/tests/compat/py39.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index 93fb699c..81ec9aac 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -2,7 +2,6 @@ Backward-compatability shims to support Python 3.9 and earlier. """ -import os import types from jaraco.collections import Projection @@ -29,12 +28,3 @@ def from_test_support(*names): from test.support import os_helper # type: ignore except ImportError: os_helper = from_test_support('temp_dir') - - -try: - from test.support.os_helper import unlink -except ImportError: - from test.support import unlink as _unlink - - def unlink(target): - return _unlink(os.fspath(target)) From 99a41c1fe19aa9f39ec88a69a66a6298837db4d2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 16:17:08 -0500 Subject: [PATCH 355/496] Re-use isolated_modules in ZipSetupBase. --- importlib_resources/tests/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index ee0aa0ad..fb827d2f 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -148,8 +148,7 @@ def setUp(self): self.fixtures = contextlib.ExitStack() self.addCleanup(self.fixtures.close) - modules = import_helper.modules_setup() - self.addCleanup(import_helper.modules_cleanup, *modules) + self.fixtures.enter_context(import_helper.isolated_modules()) temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) modules = pathlib.Path(temp_dir) / 'zipped modules.zip' From 527173b872b7673a0d43fa042dab9f095b24181f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Feb 2024 14:16:46 -0500 Subject: [PATCH 356/496] Give precedence to native traversable readers. Ensure that standard library readers are replaced while third-party readers are passed along. Closes #295. --- importlib_resources/future/adapters.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 56dea7cf..3c2d0fb3 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -1,3 +1,4 @@ +import contextlib import pathlib from contextlib import suppress from types import SimpleNamespace @@ -15,7 +16,18 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ def get_resource_reader(self, name): - return self._standard_reader() or super().get_resource_reader(name) + with contextlib.suppress(Exception): + return self._block_standard(super().get_resource_reader(name)) + return self._standard_reader() + + def _block_standard(self, reader): + """ + If the reader is from the standard library, raise an exception to + allow likely newer implementations in this library to take precedence. + """ + if reader.__class__.__module__.startswith('importlib.resources.'): + raise RuntimeError("Reader blocked to be superseded.") + return reader def _standard_reader(self): return self._zip_reader() or self._namespace_reader() or self._file_reader() From a7b1b802a29d3fa35f59162dd4a085cd38513d6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 17:49:53 -0500 Subject: [PATCH 357/496] Re-implement _block_standard as a wrapper function around _adapters.TraversableResourcesLoader and ensure that the known standard readers are replaced by readers from importlib_resources. --- importlib_resources/future/adapters.py | 51 +++++++++++++++++++------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 3c2d0fb3..9aa5b5e8 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -1,4 +1,4 @@ -import contextlib +import functools import pathlib from contextlib import suppress from types import SimpleNamespace @@ -6,6 +6,39 @@ from .. import readers, _adapters +def _block_standard(reader_getter): + """ + Wrap _adapters.TraversableResourcesLoader.get_resource_reader + and intercept any standard library readers. + """ + + @functools.wraps(reader_getter) + def wrapper(*args, **kwargs): + """ + If the reader is from the standard library, return None to allow + allow likely newer implementations in this library to take precedence. + """ + try: + reader = reader_getter(*args, **kwargs) + except NotADirectoryError: + # MultiplexedPath may fail on zip subdirectory + return + # Python 3.10+ + if reader.__class__.__module__.startswith('importlib.resources.'): + return + # Python 3.8, 3.9 + if isinstance(reader, _adapters.CompatibilityFiles) and ( + reader.spec.loader.__class__.__module__.startswith('zipimport') + or reader.spec.loader.__class__.__module__.startswith( + '_frozen_importlib_external' + ) + ): + return + return reader + + return wrapper + + class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ Adapt loaders to provide TraversableResources and other @@ -16,18 +49,10 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ def get_resource_reader(self, name): - with contextlib.suppress(Exception): - return self._block_standard(super().get_resource_reader(name)) - return self._standard_reader() - - def _block_standard(self, reader): - """ - If the reader is from the standard library, raise an exception to - allow likely newer implementations in this library to take precedence. - """ - if reader.__class__.__module__.startswith('importlib.resources.'): - raise RuntimeError("Reader blocked to be superseded.") - return reader + return ( + _block_standard(super().get_resource_reader)(name) + or self._standard_reader() + ) def _standard_reader(self): return self._zip_reader() or self._namespace_reader() or self._file_reader() From 6d48bc93d76431b5538250d236c1c54c53f0fdcd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 17:54:26 -0500 Subject: [PATCH 358/496] Add news fragment. --- newsfragments/295.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/295.feature.rst diff --git a/newsfragments/295.feature.rst b/newsfragments/295.feature.rst new file mode 100644 index 00000000..3d742533 --- /dev/null +++ b/newsfragments/295.feature.rst @@ -0,0 +1 @@ +Future compatibility adapters now ensure that standard library readers are replaced without overriding non-standard readers. From 56bd2b9cb41f76db5b7ef057cadd6d056aadf21f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 7 Mar 2024 08:10:44 -0500 Subject: [PATCH 359/496] Finalize --- NEWS.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index dbfc2227..adc804c7 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,9 @@ +v6.1.3 +====== + +No significant changes. + + v6.1.2 ====== From 15ef1e12b87238738a8b73ec56d14d4f16c9d81f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 12 Mar 2024 15:29:05 -0400 Subject: [PATCH 360/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/295.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/295.feature.rst diff --git a/NEWS.rst b/NEWS.rst index adc804c7..fd9a1f1b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.2.0 +====== + +Features +-------- + +- Future compatibility adapters now ensure that standard library readers are replaced without overriding non-standard readers. (#295) + + v6.1.3 ====== diff --git a/newsfragments/295.feature.rst b/newsfragments/295.feature.rst deleted file mode 100644 index 3d742533..00000000 --- a/newsfragments/295.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Future compatibility adapters now ensure that standard library readers are replaced without overriding non-standard readers. From 3209ccec86bc4dce93ca86fe7303813c28676d0c Mon Sep 17 00:00:00 2001 From: Mike Zimin <122507876+mikeziminio@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:55:59 +0300 Subject: [PATCH 361/496] gh-113238: add Anchor to importlib.resources (python/cpython#113801) Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> --- importlib_resources/__init__.py | 2 ++ newsfragments/+.feature.rst | 1 + 2 files changed, 3 insertions(+) create mode 100644 newsfragments/+.feature.rst diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index e6b60c18..ae83cd07 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -4,6 +4,7 @@ as_file, files, Package, + Anchor, ) from .abc import ResourceReader @@ -11,6 +12,7 @@ __all__ = [ 'Package', + 'Anchor', 'ResourceReader', 'as_file', 'files', diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst new file mode 100644 index 00000000..51b4d144 --- /dev/null +++ b/newsfragments/+.feature.rst @@ -0,0 +1 @@ +Add ``Anchor`` to ``importlib.resources`` (in order for the code to comply with the documentation) From 9f5b437d9af146edde9e4d1f2757cb808b5c3803 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 12 Mar 2024 16:35:35 -0400 Subject: [PATCH 362/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+.feature.rst diff --git a/NEWS.rst b/NEWS.rst index fd9a1f1b..fadd7e80 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.3.0 +====== + +Features +-------- + +- Add ``Anchor`` to ``importlib.resources`` (in order for the code to comply with the documentation) + + v6.2.0 ====== diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst deleted file mode 100644 index 51b4d144..00000000 --- a/newsfragments/+.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``Anchor`` to ``importlib.resources`` (in order for the code to comply with the documentation) From 2cec87b8226a3808c3047cb0c8272dac542588f3 Mon Sep 17 00:00:00 2001 From: "Loucky, Jakub" Date: Sat, 16 Mar 2024 10:35:01 -0400 Subject: [PATCH 363/496] Add test capturing missed expectation. Ref #257. --- importlib_resources/tests/test_files.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 9e45f701..3e86ec64 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -34,6 +34,11 @@ def test_read_text(self): def test_traversable(self): assert isinstance(resources.files(self.data), Traversable) + def test_joinpath_with_multiple_args(self): + files = resources.files(self.data) + binfile = files.joinpath('subdirectory', 'binary.file') + self.assertTrue(binfile.is_file()) + def test_old_parameter(self): """ Files used to take a 'package' parameter. Make sure anyone From d7b6cfbf73317aaa69500444811a44c94eb75a95 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Mar 2024 10:38:17 -0400 Subject: [PATCH 364/496] Mark test as xfail on Python 3.10 where it fails. Ref #257. --- importlib_resources/tests/test_files.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 3e86ec64..1d28179b 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,6 +4,8 @@ import importlib import contextlib +import pytest + import importlib_resources as resources from ..abc import Traversable from . import data01 @@ -34,6 +36,7 @@ def test_read_text(self): def test_traversable(self): assert isinstance(resources.files(self.data), Traversable) + @pytest.mark.xfail("sys.version_info[:2] == (3, 10)", reason="#257") def test_joinpath_with_multiple_args(self): files = resources.files(self.data) binfile = files.joinpath('subdirectory', 'binary.file') From d9e44b65dfb6a51f2ec9ee1e041ff46ca75befef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Mar 2024 11:01:33 -0400 Subject: [PATCH 365/496] Ensure stdlib readers are excluded on Python 3.10 even when found in importlib.readers. Closes #257. --- importlib_resources/future/adapters.py | 3 ++- importlib_resources/tests/test_files.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 9aa5b5e8..9d26bf47 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -24,7 +24,8 @@ def wrapper(*args, **kwargs): # MultiplexedPath may fail on zip subdirectory return # Python 3.10+ - if reader.__class__.__module__.startswith('importlib.resources.'): + mod_name = reader.__class__.__module__ + if mod_name.startswith('importlib.') and mod_name.endswith('readers'): return # Python 3.8, 3.9 if isinstance(reader, _adapters.CompatibilityFiles) and ( diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 1d28179b..3e86ec64 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,8 +4,6 @@ import importlib import contextlib -import pytest - import importlib_resources as resources from ..abc import Traversable from . import data01 @@ -36,7 +34,6 @@ def test_read_text(self): def test_traversable(self): assert isinstance(resources.files(self.data), Traversable) - @pytest.mark.xfail("sys.version_info[:2] == (3, 10)", reason="#257") def test_joinpath_with_multiple_args(self): files = resources.files(self.data) binfile = files.joinpath('subdirectory', 'binary.file') From ef414edbc74feca0ed1dfa9d00fc3df435eb3169 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Mar 2024 11:02:59 -0400 Subject: [PATCH 366/496] Add news fragment. --- newsfragments/257.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/257.bugfix.rst diff --git a/newsfragments/257.bugfix.rst b/newsfragments/257.bugfix.rst new file mode 100644 index 00000000..d8feffd9 --- /dev/null +++ b/newsfragments/257.bugfix.rst @@ -0,0 +1 @@ +Restored expectation that stdlib readers are suppressed on Python 3.10. \ No newline at end of file From 705a43514e21a1c9768f5d9b7dee952bc015c4b7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Mar 2024 11:07:45 -0400 Subject: [PATCH 367/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/257.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/257.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index fadd7e80..2a7b2c82 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.3.1 +====== + +Bugfixes +-------- + +- Restored expectation that stdlib readers are suppressed on Python 3.10. (#257) + + v6.3.0 ====== diff --git a/newsfragments/257.bugfix.rst b/newsfragments/257.bugfix.rst deleted file mode 100644 index d8feffd9..00000000 --- a/newsfragments/257.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restored expectation that stdlib readers are suppressed on Python 3.10. \ No newline at end of file From 0c3f294d711c52f538dfc47a239368c136115ff5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Mar 2024 11:34:32 -0400 Subject: [PATCH 368/496] When blocking stdlib readers, avoid giving deference to degenerate readers and instead prefer the standard readers before once again falling back to any CompatibilityFiles reader. Closes #298. --- importlib_resources/future/adapters.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 9d26bf47..d0e39242 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -40,6 +40,16 @@ def wrapper(*args, **kwargs): return wrapper +def _skip_degenerate(reader): + """ + Mask any degenerate reader. + """ + is_degenerate = ( + isinstance(reader, _adapters.CompatibilityFiles) and not reader._reader + ) + return reader if not is_degenerate else None + + class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): """ Adapt loaders to provide TraversableResources and other @@ -51,8 +61,9 @@ class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): def get_resource_reader(self, name): return ( - _block_standard(super().get_resource_reader)(name) + _skip_degenerate(_block_standard(super().get_resource_reader)(name)) or self._standard_reader() + or super().get_resource_reader(name) ) def _standard_reader(self): From cee77e81a775050695544d341477254ba52fb035 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Mar 2024 11:39:21 -0400 Subject: [PATCH 369/496] Add news fragment. --- newsfragments/298.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/298.bugfix.rst diff --git a/newsfragments/298.bugfix.rst b/newsfragments/298.bugfix.rst new file mode 100644 index 00000000..db893193 --- /dev/null +++ b/newsfragments/298.bugfix.rst @@ -0,0 +1 @@ +Restored expectation that local standard readers are preferred over degenerate readers. \ No newline at end of file From 0f8aa5fe2ebb9cd40cdf4c83f7855cdbe4f642da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Mar 2024 11:43:37 -0400 Subject: [PATCH 370/496] Add reference to the issue in the docstring. --- importlib_resources/future/adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index d0e39242..0e9764ba 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -42,7 +42,7 @@ def wrapper(*args, **kwargs): def _skip_degenerate(reader): """ - Mask any degenerate reader. + Mask any degenerate reader. Ref #298. """ is_degenerate = ( isinstance(reader, _adapters.CompatibilityFiles) and not reader._reader From a3d6fdb26308956571d804aff4cdb75e77b6cdcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Mar 2024 11:45:21 -0400 Subject: [PATCH 371/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/298.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/298.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 2a7b2c82..4404e903 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.3.2 +====== + +Bugfixes +-------- + +- Restored expectation that local standard readers are preferred over degenerate readers. (#298) + + v6.3.1 ====== diff --git a/newsfragments/298.bugfix.rst b/newsfragments/298.bugfix.rst deleted file mode 100644 index db893193..00000000 --- a/newsfragments/298.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Restored expectation that local standard readers are preferred over degenerate readers. \ No newline at end of file From 74670d4fd43ae1ae5e1b51dbfc6c89408431eaaa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 04:54:18 -0400 Subject: [PATCH 372/496] Add support for python/cpython references --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c9b47e44..0dbf234f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(Python #|bpo-)(?P\d+)', + pattern=r'(python/cpython#|Python #|bpo-)(?P\d+)', url='http://bugs.python.org/issue{python}', ), ], From 0db550c38ad9ba4ab4fb6fa8c070c040935e2f33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 06:20:54 -0400 Subject: [PATCH 373/496] Consolidated test support logic in jaraco.test.cpython. --- importlib_resources/tests/compat/py39.py | 30 ++++-------------------- setup.cfg | 2 +- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index 81ec9aac..e158eb85 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -2,29 +2,9 @@ Backward-compatability shims to support Python 3.9 and earlier. """ -import types +from jaraco.test.cpython import from_test_support, try_import -from jaraco.collections import Projection - - -def from_test_support(*names): - """ - Return a SimpleNamespace of names from test.support. - """ - import test.support - - return types.SimpleNamespace(**Projection(names, vars(test.support))) - - -try: - from test.support import import_helper # type: ignore -except ImportError: - import_helper = from_test_support( - 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' - ) - - -try: - from test.support import os_helper # type: ignore -except ImportError: - os_helper = from_test_support('temp_dir') +import_helper = try_import('import_helper') or from_test_support( + 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' +) +os_helper = try_import('os_helper') or from_test_support('temp_dir') diff --git a/setup.cfg b/setup.cfg index feef5904..e684c200 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ testing = # local zipp >= 3.17 - jaraco.collections + jaraco.test >= 5.4 docs = # upstream From 189d15fcbe99a1ec624fb19992544096e2a538f3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 21 Mar 2024 10:11:18 +0100 Subject: [PATCH 374/496] Apply CPython PR, sans docs and changelogs --- importlib_resources/__init__.py | 17 ++ importlib_resources/functional.py | 88 ++++++++ importlib_resources/tests/test_functional.py | 217 +++++++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 importlib_resources/functional.py create mode 100644 importlib_resources/tests/test_functional.py diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index ae83cd07..0d029abd 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -7,6 +7,16 @@ Anchor, ) +from .functional import ( + contents, + is_resource, + open_binary, + open_text, + path, + read_binary, + read_text, +) + from .abc import ResourceReader @@ -16,4 +26,11 @@ 'ResourceReader', 'as_file', 'files', + 'contents', + 'is_resource', + 'open_binary', + 'open_text', + 'path', + 'read_binary', + 'read_text', ] diff --git a/importlib_resources/functional.py b/importlib_resources/functional.py new file mode 100644 index 00000000..80c192f9 --- /dev/null +++ b/importlib_resources/functional.py @@ -0,0 +1,88 @@ +"""Simplified function-based API for importlib.resources + + +""" + +import os +import warnings + +from ._common import files, as_file + + +_MISSING = object() + +def open_binary(anchor, *path_names): + """Open for binary reading the *resource* within *package*.""" + return _get_resource(anchor, path_names).open('rb') + + +def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'): + """Open for text reading the *resource* within *package*.""" + encoding = _get_encoding_arg(path_names, encoding) + resource = _get_resource(anchor, path_names) + return resource.open('r', encoding=encoding, errors=errors) + + +def read_binary(anchor, *path_names): + """Read and return contents of *resource* within *package* as bytes.""" + return _get_resource(anchor, path_names).read_bytes() + + +def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'): + """Read and return contents of *resource* within *package* as str.""" + encoding = _get_encoding_arg(path_names, encoding) + resource = _get_resource(anchor, path_names) + return resource.read_text(encoding=encoding, errors=errors) + + +def path(anchor, *path_names): + """Return the path to the *resource* as an actual file system path.""" + return as_file(_get_resource(anchor, path_names)) + + +def is_resource(anchor, *path_names): + """Return ``True`` if there is a resource named *name* in the package, + + Otherwise returns ``False``. + """ + return _get_resource(anchor, path_names).is_file() + + +def contents(anchor, *path_names): + """Return an iterable over the named resources within the package. + + The iterable returns :class:`str` resources (e.g. files). + The iterable does not recurse into subdirectories. + """ + warnings.warn( + "importlib.resources.contents is deprecated. " + "Use files(anchor).iterdir() instead.", + DeprecationWarning, + stacklevel=1, + ) + return ( + resource.name + for resource + in _get_resource(anchor, path_names).iterdir() + ) + + +def _get_encoding_arg(path_names, encoding): + # For compatibility with versions where *encoding* was a positional + # argument, it needs to be given explicitly when there are multiple + # *path_names*. + # This limitation can be removed in Python 3.15. + if encoding is _MISSING: + if len(path_names) > 1: + raise TypeError( + "'encoding' argument required with multiple path names", + ) + else: + return 'utf-8' + return encoding + + +def _get_resource(anchor, path_names): + if anchor is None: + raise TypeError("anchor must be module or string, got None") + return files(anchor).joinpath(*path_names) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py new file mode 100644 index 00000000..cc7199d1 --- /dev/null +++ b/importlib_resources/tests/test_functional.py @@ -0,0 +1,217 @@ +import unittest +import os + +from test.support.warnings_helper import ignore_warnings, check_warnings + +import importlib.resources + +# Since the functional API forwards to Traversable, we only test +# filesystem resources here -- not zip files, namespace packages etc. +# We do test for two kinds of Anchor, though. + + +class StringAnchorMixin: + anchor01 = 'test.test_importlib.resources.data01' + anchor02 = 'test.test_importlib.resources.data02' + + +class ModuleAnchorMixin: + from test.test_importlib.resources import data01 as anchor01 + from test.test_importlib.resources import data02 as anchor02 + + +class FunctionalAPIBase(): + def _gen_resourcetxt_path_parts(self): + """Yield various names of a text file in anchor02, each in a subTest + """ + for path_parts in ( + ('subdirectory', 'subsubdir', 'resource.txt'), + ('subdirectory/subsubdir/resource.txt',), + ('subdirectory/subsubdir', 'resource.txt'), + ): + with self.subTest(path_parts=path_parts): + yield path_parts + + def test_read_text(self): + self.assertEqual( + importlib.resources.read_text(self.anchor01, 'utf-8.file'), + 'Hello, UTF-8 world!\n', + ) + self.assertEqual( + importlib.resources.read_text( + self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt', + encoding='utf-8', + ), + 'a resource', + ) + for path_parts in self._gen_resourcetxt_path_parts(): + self.assertEqual( + importlib.resources.read_text( + self.anchor02, *path_parts, encoding='utf-8', + ), + 'a resource', + ) + # Use generic OSError, since e.g. attempting to read a directory can + # fail with PermissionError rather than IsADirectoryError + with self.assertRaises(OSError): + importlib.resources.read_text(self.anchor01) + with self.assertRaises(OSError): + importlib.resources.read_text(self.anchor01, 'no-such-file') + with self.assertRaises(UnicodeDecodeError): + importlib.resources.read_text(self.anchor01, 'utf-16.file') + self.assertEqual( + importlib.resources.read_text( + self.anchor01, 'binary.file', encoding='latin1', + ), + '\x00\x01\x02\x03', + ) + self.assertEqual( + importlib.resources.read_text( + self.anchor01, 'utf-16.file', + errors='backslashreplace', + ), + 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + errors='backslashreplace', + ) + ) + + def test_read_binary(self): + self.assertEqual( + importlib.resources.read_binary(self.anchor01, 'utf-8.file'), + b'Hello, UTF-8 world!\n', + ) + for path_parts in self._gen_resourcetxt_path_parts(): + self.assertEqual( + importlib.resources.read_binary(self.anchor02, *path_parts), + b'a resource', + ) + + def test_open_text(self): + with importlib.resources.open_text(self.anchor01, 'utf-8.file') as f: + self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + for path_parts in self._gen_resourcetxt_path_parts(): + with importlib.resources.open_text( + self.anchor02, *path_parts, + encoding='utf-8', + ) as f: + self.assertEqual(f.read(), 'a resource') + # Use generic OSError, since e.g. attempting to read a directory can + # fail with PermissionError rather than IsADirectoryError + with self.assertRaises(OSError): + importlib.resources.open_text(self.anchor01) + with self.assertRaises(OSError): + importlib.resources.open_text(self.anchor01, 'no-such-file') + with importlib.resources.open_text(self.anchor01, 'utf-16.file') as f: + with self.assertRaises(UnicodeDecodeError): + f.read() + with importlib.resources.open_text( + self.anchor01, 'binary.file', encoding='latin1', + ) as f: + self.assertEqual(f.read(), '\x00\x01\x02\x03') + with importlib.resources.open_text( + self.anchor01, 'utf-16.file', + errors='backslashreplace', + ) as f: + self.assertEqual( + f.read(), + 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + errors='backslashreplace', + ) + ) + + def test_open_binary(self): + with importlib.resources.open_binary(self.anchor01, 'utf-8.file') as f: + self.assertEqual(f.read(), b'Hello, UTF-8 world!\n') + for path_parts in self._gen_resourcetxt_path_parts(): + with importlib.resources.open_binary( + self.anchor02, *path_parts, + ) as f: + self.assertEqual(f.read(), b'a resource') + + def test_path(self): + with importlib.resources.path(self.anchor01, 'utf-8.file') as path: + with open(str(path)) as f: + self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + with importlib.resources.path(self.anchor01) as path: + with open(os.path.join(path, 'utf-8.file')) as f: + self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') + + def test_is_resource(self): + is_resource = importlib.resources.is_resource + self.assertTrue(is_resource(self.anchor01, 'utf-8.file')) + self.assertFalse(is_resource(self.anchor01, 'no_such_file')) + self.assertFalse(is_resource(self.anchor01)) + self.assertFalse(is_resource(self.anchor01, 'subdirectory')) + for path_parts in self._gen_resourcetxt_path_parts(): + self.assertTrue(is_resource(self.anchor02, *path_parts)) + + def test_contents(self): + is_resource = importlib.resources.is_resource + with check_warnings((".*contents.*", DeprecationWarning)): + c = importlib.resources.contents(self.anchor01) + self.assertGreaterEqual( + set(c), + {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, + ) + with (self.assertRaises(OSError), + check_warnings((".*contents.*", DeprecationWarning)), + ): + importlib.resources.contents(self.anchor01, 'utf-8.file') + for path_parts in self._gen_resourcetxt_path_parts(): + with (self.assertRaises(OSError), + check_warnings((".*contents.*", DeprecationWarning)), + ): + importlib.resources.contents(self.anchor01, *path_parts) + with check_warnings((".*contents.*", DeprecationWarning)): + c = importlib.resources.contents(self.anchor01, 'subdirectory') + self.assertGreaterEqual( + set(c), + {'binary.file'}, + ) + + @ignore_warnings(category=DeprecationWarning) + def test_common_errors(self): + for func in ( + importlib.resources.read_text, + importlib.resources.read_binary, + importlib.resources.open_text, + importlib.resources.open_binary, + importlib.resources.path, + importlib.resources.is_resource, + importlib.resources.contents, + ): + with self.subTest(func=func): + # Rejecting None anchor + with self.assertRaises(TypeError): + func(None) + # Rejecting invalid anchor type + with self.assertRaises((TypeError, AttributeError)): + func(1234) + # Unknown module + with self.assertRaises(ModuleNotFoundError): + func('$missing module$') + + def test_text_errors(self): + for func in ( + importlib.resources.read_text, + importlib.resources.open_text, + ): + with self.subTest(func=func): + # Multiple path arguments need explicit encoding argument. + with self.assertRaises(TypeError): + func( + self.anchor02, 'subdirectory', + 'subsubdir', 'resource.txt', + ) + + +class FunctionalAPITest_StringAnchor( + unittest.TestCase, FunctionalAPIBase, StringAnchorMixin, +): + pass + + +class FunctionalAPITest_ModuleAnchor( + unittest.TestCase, FunctionalAPIBase, ModuleAnchorMixin, +): + pass From 1e98e351779d53092d7988d362503b54b3dc6b35 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 21 Mar 2024 10:46:17 +0100 Subject: [PATCH 375/496] Adapt to importlib_resources --- importlib_resources/functional.py | 3 - importlib_resources/tests/test_functional.py | 91 ++++++++++---------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/importlib_resources/functional.py b/importlib_resources/functional.py index 80c192f9..9f016801 100644 --- a/importlib_resources/functional.py +++ b/importlib_resources/functional.py @@ -1,9 +1,6 @@ """Simplified function-based API for importlib.resources - - """ -import os import warnings from ._common import files, as_file diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index cc7199d1..9a9b4e16 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -3,7 +3,7 @@ from test.support.warnings_helper import ignore_warnings, check_warnings -import importlib.resources +import importlib_resources as resources # Since the functional API forwards to Traversable, we only test # filesystem resources here -- not zip files, namespace packages etc. @@ -11,13 +11,13 @@ class StringAnchorMixin: - anchor01 = 'test.test_importlib.resources.data01' - anchor02 = 'test.test_importlib.resources.data02' + anchor01 = 'importlib_resources.tests.data01' + anchor02 = 'importlib_resources.tests.data02' class ModuleAnchorMixin: - from test.test_importlib.resources import data01 as anchor01 - from test.test_importlib.resources import data02 as anchor02 + from . import data01 as anchor01 + from . import data02 as anchor02 class FunctionalAPIBase(): @@ -34,11 +34,11 @@ def _gen_resourcetxt_path_parts(self): def test_read_text(self): self.assertEqual( - importlib.resources.read_text(self.anchor01, 'utf-8.file'), + resources.read_text(self.anchor01, 'utf-8.file'), 'Hello, UTF-8 world!\n', ) self.assertEqual( - importlib.resources.read_text( + resources.read_text( self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt', encoding='utf-8', ), @@ -46,7 +46,7 @@ def test_read_text(self): ) for path_parts in self._gen_resourcetxt_path_parts(): self.assertEqual( - importlib.resources.read_text( + resources.read_text( self.anchor02, *path_parts, encoding='utf-8', ), 'a resource', @@ -54,19 +54,19 @@ def test_read_text(self): # Use generic OSError, since e.g. attempting to read a directory can # fail with PermissionError rather than IsADirectoryError with self.assertRaises(OSError): - importlib.resources.read_text(self.anchor01) + resources.read_text(self.anchor01) with self.assertRaises(OSError): - importlib.resources.read_text(self.anchor01, 'no-such-file') + resources.read_text(self.anchor01, 'no-such-file') with self.assertRaises(UnicodeDecodeError): - importlib.resources.read_text(self.anchor01, 'utf-16.file') + resources.read_text(self.anchor01, 'utf-16.file') self.assertEqual( - importlib.resources.read_text( + resources.read_text( self.anchor01, 'binary.file', encoding='latin1', ), '\x00\x01\x02\x03', ) self.assertEqual( - importlib.resources.read_text( + resources.read_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ), @@ -77,20 +77,20 @@ def test_read_text(self): def test_read_binary(self): self.assertEqual( - importlib.resources.read_binary(self.anchor01, 'utf-8.file'), + resources.read_binary(self.anchor01, 'utf-8.file'), b'Hello, UTF-8 world!\n', ) for path_parts in self._gen_resourcetxt_path_parts(): self.assertEqual( - importlib.resources.read_binary(self.anchor02, *path_parts), + resources.read_binary(self.anchor02, *path_parts), b'a resource', ) def test_open_text(self): - with importlib.resources.open_text(self.anchor01, 'utf-8.file') as f: + with resources.open_text(self.anchor01, 'utf-8.file') as f: self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') for path_parts in self._gen_resourcetxt_path_parts(): - with importlib.resources.open_text( + with resources.open_text( self.anchor02, *path_parts, encoding='utf-8', ) as f: @@ -98,17 +98,17 @@ def test_open_text(self): # Use generic OSError, since e.g. attempting to read a directory can # fail with PermissionError rather than IsADirectoryError with self.assertRaises(OSError): - importlib.resources.open_text(self.anchor01) + resources.open_text(self.anchor01) with self.assertRaises(OSError): - importlib.resources.open_text(self.anchor01, 'no-such-file') - with importlib.resources.open_text(self.anchor01, 'utf-16.file') as f: + resources.open_text(self.anchor01, 'no-such-file') + with resources.open_text(self.anchor01, 'utf-16.file') as f: with self.assertRaises(UnicodeDecodeError): f.read() - with importlib.resources.open_text( + with resources.open_text( self.anchor01, 'binary.file', encoding='latin1', ) as f: self.assertEqual(f.read(), '\x00\x01\x02\x03') - with importlib.resources.open_text( + with resources.open_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ) as f: @@ -120,24 +120,24 @@ def test_open_text(self): ) def test_open_binary(self): - with importlib.resources.open_binary(self.anchor01, 'utf-8.file') as f: + with resources.open_binary(self.anchor01, 'utf-8.file') as f: self.assertEqual(f.read(), b'Hello, UTF-8 world!\n') for path_parts in self._gen_resourcetxt_path_parts(): - with importlib.resources.open_binary( + with resources.open_binary( self.anchor02, *path_parts, ) as f: self.assertEqual(f.read(), b'a resource') def test_path(self): - with importlib.resources.path(self.anchor01, 'utf-8.file') as path: - with open(str(path)) as f: + with resources.path(self.anchor01, 'utf-8.file') as path: + with open(str(path), encoding='utf-8') as f: self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') - with importlib.resources.path(self.anchor01) as path: - with open(os.path.join(path, 'utf-8.file')) as f: + with resources.path(self.anchor01) as path: + with open(os.path.join(path, 'utf-8.file'), encoding='utf-8') as f: self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') def test_is_resource(self): - is_resource = importlib.resources.is_resource + is_resource = resources.is_resource self.assertTrue(is_resource(self.anchor01, 'utf-8.file')) self.assertFalse(is_resource(self.anchor01, 'no_such_file')) self.assertFalse(is_resource(self.anchor01)) @@ -146,24 +146,25 @@ def test_is_resource(self): self.assertTrue(is_resource(self.anchor02, *path_parts)) def test_contents(self): - is_resource = importlib.resources.is_resource with check_warnings((".*contents.*", DeprecationWarning)): - c = importlib.resources.contents(self.anchor01) + c = resources.contents(self.anchor01) self.assertGreaterEqual( set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with (self.assertRaises(OSError), + with ( + self.assertRaises(OSError), check_warnings((".*contents.*", DeprecationWarning)), ): - importlib.resources.contents(self.anchor01, 'utf-8.file') + list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): - with (self.assertRaises(OSError), + with ( + self.assertRaises(OSError), check_warnings((".*contents.*", DeprecationWarning)), ): - importlib.resources.contents(self.anchor01, *path_parts) + list(resources.contents(self.anchor01, *path_parts)) with check_warnings((".*contents.*", DeprecationWarning)): - c = importlib.resources.contents(self.anchor01, 'subdirectory') + c = resources.contents(self.anchor01, 'subdirectory') self.assertGreaterEqual( set(c), {'binary.file'}, @@ -172,13 +173,13 @@ def test_contents(self): @ignore_warnings(category=DeprecationWarning) def test_common_errors(self): for func in ( - importlib.resources.read_text, - importlib.resources.read_binary, - importlib.resources.open_text, - importlib.resources.open_binary, - importlib.resources.path, - importlib.resources.is_resource, - importlib.resources.contents, + resources.read_text, + resources.read_binary, + resources.open_text, + resources.open_binary, + resources.path, + resources.is_resource, + resources.contents, ): with self.subTest(func=func): # Rejecting None anchor @@ -193,8 +194,8 @@ def test_common_errors(self): def test_text_errors(self): for func in ( - importlib.resources.read_text, - importlib.resources.open_text, + resources.read_text, + resources.open_text, ): with self.subTest(func=func): # Multiple path arguments need explicit encoding argument. From 558f5bf9f266998e616deaf8a9d373da37b33054 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 21 Mar 2024 10:52:42 +0100 Subject: [PATCH 376/496] Formatting nitpicks --- importlib_resources/functional.py | 4 ++-- importlib_resources/tests/test_functional.py | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/importlib_resources/functional.py b/importlib_resources/functional.py index 9f016801..9e3ea154 100644 --- a/importlib_resources/functional.py +++ b/importlib_resources/functional.py @@ -1,5 +1,4 @@ -"""Simplified function-based API for importlib.resources -""" +"""Simplified function-based API for importlib.resources""" import warnings @@ -8,6 +7,7 @@ _MISSING = object() + def open_binary(anchor, *path_names): """Open for binary reading the *resource* within *package*.""" return _get_resource(anchor, path_names).open('rb') diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 9a9b4e16..f359900a 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -20,7 +20,7 @@ class ModuleAnchorMixin: from . import data02 as anchor02 -class FunctionalAPIBase(): +class FunctionalAPIBase: def _gen_resourcetxt_path_parts(self): """Yield various names of a text file in anchor02, each in a subTest """ @@ -61,18 +61,21 @@ def test_read_text(self): resources.read_text(self.anchor01, 'utf-16.file') self.assertEqual( resources.read_text( - self.anchor01, 'binary.file', encoding='latin1', + self.anchor01, + 'binary.file', + encoding='latin1', ), '\x00\x01\x02\x03', ) self.assertEqual( resources.read_text( - self.anchor01, 'utf-16.file', + self.anchor01, + 'utf-16.file', errors='backslashreplace', ), 'Hello, UTF-16 world!\n'.encode('utf-16').decode( errors='backslashreplace', - ) + ), ) def test_read_binary(self): @@ -105,18 +108,21 @@ def test_open_text(self): with self.assertRaises(UnicodeDecodeError): f.read() with resources.open_text( - self.anchor01, 'binary.file', encoding='latin1', + self.anchor01, + 'binary.file', + encoding='latin1', ) as f: self.assertEqual(f.read(), '\x00\x01\x02\x03') with resources.open_text( - self.anchor01, 'utf-16.file', + self.anchor01, + 'utf-16.file', errors='backslashreplace', ) as f: self.assertEqual( f.read(), 'Hello, UTF-16 world!\n'.encode('utf-16').decode( errors='backslashreplace', - ) + ), ) def test_open_binary(self): From 8fdadde235ce5d5d8f80934bb2f9c9b29cb0da78 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 21 Mar 2024 11:06:01 +0100 Subject: [PATCH 377/496] Port tests to Python 3.8 --- importlib_resources/tests/test_functional.py | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index f359900a..39a8b5d6 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -1,7 +1,12 @@ import unittest import os +import contextlib -from test.support.warnings_helper import ignore_warnings, check_warnings +try: + from test.support.warnings_helper import ignore_warnings, check_warnings +except ImportError: + # older Python versions + from test.support import ignore_warnings, check_warnings import importlib_resources as resources @@ -158,16 +163,17 @@ def test_contents(self): set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with ( - self.assertRaises(OSError), - check_warnings((".*contents.*", DeprecationWarning)), - ): + with contextlib.ExitStack() as cm: + cm.enter_context(self.assertRaises(OSError)) + cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) + list(resources.contents(self.anchor01, 'utf-8.file')) + for path_parts in self._gen_resourcetxt_path_parts(): - with ( - self.assertRaises(OSError), - check_warnings((".*contents.*", DeprecationWarning)), - ): + with contextlib.ExitStack() as cm: + cm.enter_context(self.assertRaises(OSError)) + cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) + list(resources.contents(self.anchor01, *path_parts)) with check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') From 2df6ced9be7d45cdc443ceb8c7e66ab846d19ebc Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 21 Mar 2024 11:09:55 +0100 Subject: [PATCH 378/496] Use Ruff style, rather than PEP 8 --- importlib_resources/functional.py | 6 +--- importlib_resources/tests/test_functional.py | 32 ++++++++++++++------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/importlib_resources/functional.py b/importlib_resources/functional.py index 9e3ea154..f59416f2 100644 --- a/importlib_resources/functional.py +++ b/importlib_resources/functional.py @@ -57,11 +57,7 @@ def contents(anchor, *path_names): DeprecationWarning, stacklevel=1, ) - return ( - resource.name - for resource - in _get_resource(anchor, path_names).iterdir() - ) + return (resource.name for resource in _get_resource(anchor, path_names).iterdir()) def _get_encoding_arg(path_names, encoding): diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 39a8b5d6..69706cf7 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -27,8 +27,7 @@ class ModuleAnchorMixin: class FunctionalAPIBase: def _gen_resourcetxt_path_parts(self): - """Yield various names of a text file in anchor02, each in a subTest - """ + """Yield various names of a text file in anchor02, each in a subTest""" for path_parts in ( ('subdirectory', 'subsubdir', 'resource.txt'), ('subdirectory/subsubdir/resource.txt',), @@ -44,7 +43,10 @@ def test_read_text(self): ) self.assertEqual( resources.read_text( - self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt', + self.anchor02, + 'subdirectory', + 'subsubdir', + 'resource.txt', encoding='utf-8', ), 'a resource', @@ -52,7 +54,9 @@ def test_read_text(self): for path_parts in self._gen_resourcetxt_path_parts(): self.assertEqual( resources.read_text( - self.anchor02, *path_parts, encoding='utf-8', + self.anchor02, + *path_parts, + encoding='utf-8', ), 'a resource', ) @@ -99,7 +103,8 @@ def test_open_text(self): self.assertEqual(f.read(), 'Hello, UTF-8 world!\n') for path_parts in self._gen_resourcetxt_path_parts(): with resources.open_text( - self.anchor02, *path_parts, + self.anchor02, + *path_parts, encoding='utf-8', ) as f: self.assertEqual(f.read(), 'a resource') @@ -135,7 +140,8 @@ def test_open_binary(self): self.assertEqual(f.read(), b'Hello, UTF-8 world!\n') for path_parts in self._gen_resourcetxt_path_parts(): with resources.open_binary( - self.anchor02, *path_parts, + self.anchor02, + *path_parts, ) as f: self.assertEqual(f.read(), b'a resource') @@ -213,18 +219,24 @@ def test_text_errors(self): # Multiple path arguments need explicit encoding argument. with self.assertRaises(TypeError): func( - self.anchor02, 'subdirectory', - 'subsubdir', 'resource.txt', + self.anchor02, + 'subdirectory', + 'subsubdir', + 'resource.txt', ) class FunctionalAPITest_StringAnchor( - unittest.TestCase, FunctionalAPIBase, StringAnchorMixin, + unittest.TestCase, + FunctionalAPIBase, + StringAnchorMixin, ): pass class FunctionalAPITest_ModuleAnchor( - unittest.TestCase, FunctionalAPIBase, ModuleAnchorMixin, + unittest.TestCase, + FunctionalAPIBase, + ModuleAnchorMixin, ): pass From ca03a4df06b8794810ad5c4d34265d40fb4b3f49 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 21 Mar 2024 03:49:10 +0000 Subject: [PATCH 379/496] GH-109653: Defer import of ``importlib.metadata._adapters`` (python/cpython#109829) --------- Co-authored-by: Jason R. Coombs --- importlib_resources/_common.py | 5 +++-- newsfragments/+.feature.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 newsfragments/+.feature.rst diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index c9891359..8df6b39e 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -12,8 +12,6 @@ from typing import Union, Optional, cast from .abc import ResourceReader, Traversable -from .future.adapters import wrap_spec - Package = Union[types.ModuleType, str] Anchor = Package @@ -111,6 +109,9 @@ def from_package(package: types.ModuleType): Return a Traversable object for the given package. """ + # deferred for performance (python/cpython#109829) + from .future.adapters import wrap_spec + spec = wrap_spec(package) reader = spec.loader.get_resource_reader(spec.name) return reader.files() diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst new file mode 100644 index 00000000..976f76f3 --- /dev/null +++ b/newsfragments/+.feature.rst @@ -0,0 +1 @@ +Deferred select imports in for a speedup (python/cpython#109829). From fa60969a37ed01302d2ed01956e8ef18eba87923 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 09:34:23 -0400 Subject: [PATCH 380/496] Add news fragment. --- newsfragments/303.feature.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 newsfragments/303.feature.rst diff --git a/newsfragments/303.feature.rst b/newsfragments/303.feature.rst new file mode 100644 index 00000000..537a0c29 --- /dev/null +++ b/newsfragments/303.feature.rst @@ -0,0 +1,10 @@ +The functions +``is_resource()``, +``open_binary()``, +``open_text()``, +``path()``, +``read_binary()``, and +``read_text()`` are un-deprecated, and support +subdirectories via multiple positional arguments. +The ``contents()`` function also allows subdirectories, +but remains deprecated. From 1f4d3f10a3ed5d65b3092a39369c08e71e30a97c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 09:35:24 -0400 Subject: [PATCH 381/496] Finalize --- NEWS.rst | 19 +++++++++++++++++++ newsfragments/+.feature.rst | 1 - newsfragments/303.feature.rst | 10 ---------- 3 files changed, 19 insertions(+), 11 deletions(-) delete mode 100644 newsfragments/+.feature.rst delete mode 100644 newsfragments/303.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 4404e903..c4a6f36c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,22 @@ +v6.4.0 +====== + +Features +-------- + +- The functions + ``is_resource()``, + ``open_binary()``, + ``open_text()``, + ``path()``, + ``read_binary()``, and + ``read_text()`` are un-deprecated, and support + subdirectories via multiple positional arguments. + The ``contents()`` function also allows subdirectories, + but remains deprecated. (#303) +- Deferred select imports in for a speedup (python/cpython#109829). + + v6.3.2 ====== diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst deleted file mode 100644 index 976f76f3..00000000 --- a/newsfragments/+.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred select imports in for a speedup (python/cpython#109829). diff --git a/newsfragments/303.feature.rst b/newsfragments/303.feature.rst deleted file mode 100644 index 537a0c29..00000000 --- a/newsfragments/303.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -The functions -``is_resource()``, -``open_binary()``, -``open_text()``, -``path()``, -``read_binary()``, and -``read_text()`` are un-deprecated, and support -subdirectories via multiple positional arguments. -The ``contents()`` function also allows subdirectories, -but remains deprecated. From 6fa0a7aef25178430816c4459524a4945bfb7ee1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 15:05:00 -0400 Subject: [PATCH 382/496] Add Jason as (long time) maintainer. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index e684c200..691437ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,8 @@ name = importlib_resources author = Barry Warsaw author_email = barry@python.org +maintainer = Jason R. Coombs +maintainer_email = jaraco@jaraco.com description = Read resources from Python packages long_description = file: README.rst url = https://github.com/python/importlib_resources From a0d0c4b7e87fbfd04cee2546ba452858587516fd Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 21 Mar 2024 15:34:23 -0400 Subject: [PATCH 383/496] 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 384/496] 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 385/496] 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 386/496] 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 387/496] 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 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 388/496] 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 7ad4f2fa9fb2b030d3ecc231fc24de181705622d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 16 Apr 2024 10:31:45 -0400 Subject: [PATCH 389/496] 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 390/496] 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 d0f7efef469b731e101261a151885c39126a6888 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Apr 2024 16:37:47 -0400 Subject: [PATCH 391/496] Migrated config to pyproject.toml using jaraco.develop.migrate-config and ini2toml. --- pyproject.toml | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 54 ----------------------------------------------- 2 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index a853c578..b9d17498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,60 @@ [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 = "importlib_resources" +authors = [ + { name = "Barry Warsaw", email = "barry@python.org" }, +] +maintainers = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "Read resources from Python packages" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] +requires-python = ">=3.8" +dependencies = ["zipp >= 3.1.0; python_version < '3.10'"] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/python/importlib_resources" +Documentation = "https://importlib-resources.readthedocs.io/" + +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6", + "pytest-checkdocs >= 2.4", + "pytest-cov", + 'pytest-mypy; python_implementation != "PyPy"', # workaround for jaraco/skeleton#22 + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local + "zipp >= 3.17", + "jaraco.test >= 5.4", +] +docs = [ + # upstream + "sphinx >= 3.5", + # workaround for sphinx/sphinx-doc#11662 + "sphinx < 7.2.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 691437ac..00000000 --- a/setup.cfg +++ /dev/null @@ -1,54 +0,0 @@ -[metadata] -name = importlib_resources -author = Barry Warsaw -author_email = barry@python.org -maintainer = Jason R. Coombs -maintainer_email = jaraco@jaraco.com -description = Read resources from Python packages -long_description = file: README.rst -url = https://github.com/python/importlib_resources -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only -project_urls = - Documentation = https://importlib-resources.readthedocs.io/ - -[options] -include_package_data = true -python_requires = >=3.8 -install_requires = - zipp >= 3.1.0; python_version < '3.10' - -[options.extras_require] -testing = - # upstream - pytest >= 6 - pytest-checkdocs >= 2.4 - pytest-cov - pytest-mypy; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" - pytest-enabler >= 2.2 - pytest-ruff >= 0.2.1 - - # local - zipp >= 3.17 - jaraco.test >= 5.4 - -docs = - # upstream - sphinx >= 3.5 - # workaround for sphinx/sphinx-doc#11662 - sphinx < 7.2.5 - jaraco.packaging >= 9.3 - rst.linker >= 1.9 - furo - sphinx-lint - - # tidelift - jaraco.tidelift >= 1.4 - - # local From 744cf2a2befb6a616657c105e5c9be9f3f921224 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2024 10:48:06 -0400 Subject: [PATCH 392/496] 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 393/496] 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 394/496] 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 a595a0fad054cd20b69d3e954c99174e3a548938 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 31 May 2024 03:53:48 -0400 Subject: [PATCH 395/496] 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 c9729e1a0f66b7adad70c629518b7dab82ccd8c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Jun 2024 13:09:07 -0400 Subject: [PATCH 396/496] 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 0058f32e10384353920ff8060524c4e25fac04a6 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 22 Mar 2024 16:59:31 +0100 Subject: [PATCH 397/496] Rename functional.py to _functional.py --- importlib_resources/__init__.py | 2 +- importlib_resources/{functional.py => _functional.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename importlib_resources/{functional.py => _functional.py} (100%) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 0d029abd..ec4441c9 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -7,7 +7,7 @@ Anchor, ) -from .functional import ( +from ._functional import ( contents, is_resource, open_binary, diff --git a/importlib_resources/functional.py b/importlib_resources/_functional.py similarity index 100% rename from importlib_resources/functional.py rename to importlib_resources/_functional.py From be5a270fa1da95b5e47ca51c334c34fce4b1fac3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 29 Apr 2024 15:47:21 -0400 Subject: [PATCH 398/496] correct variable in example in migration.rst --- docs/migration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration.rst b/docs/migration.rst index fa2155de..5121d068 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -109,7 +109,7 @@ following example is often written for clarity as:: This can be easily rewritten like so:: ref = importlib_resources.files('my.package').joinpath('resource.dat') - contents = f.read_bytes() + contents = ref.read_bytes() pkg_resources.resource_listdir() From 33c4896dbaeda2fd7a5fef701431dea05bb83bab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Jul 2024 15:34:09 -0400 Subject: [PATCH 399/496] 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 400/496] "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 401/496] 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 402/496] 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 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 403/496] 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 404/496] 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 405/496] 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 406/496] 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 f77da58a0ada7f86910d60a02ab30a271b80b965 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 11:34:03 -0400 Subject: [PATCH 407/496] Remove ZipSetupBase, no longer needed. --- importlib_resources/tests/test_resource.py | 8 ++++---- importlib_resources/tests/util.py | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index dc2a108c..32aa909a 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -84,7 +84,7 @@ def test_package_has_no_reader_fallback(self): self.assertFalse(resources.files(module).joinpath('A').is_file()) -class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): +class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase): ZIP_MODULE = 'data01' def test_is_submodule_resource(self): @@ -117,7 +117,7 @@ def test_as_file_directory(self): assert not data.parent.exists() -class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): +class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase): ZIP_MODULE = 'data02' def test_unrelated_contents(self): @@ -135,7 +135,7 @@ def test_unrelated_contents(self): ) -class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase): +class DeletingZipsTest(util.ZipSetup, unittest.TestCase): """Having accessed resources in a zip file should not keep an open reference to the zip. """ @@ -230,7 +230,7 @@ def tearDownClass(cls): class ResourceFromNamespaceZipTests( - util.ZipSetupBase, + util.ZipSetup, ResourceFromNamespaceTests, unittest.TestCase, ): diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index fb827d2f..b258aec0 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -141,7 +141,7 @@ def test_useless_loader(self): self.execute(package, 'utf-8.file') -class ZipSetupBase: +class ZipSetup: ZIP_MODULE = 'data01' def setUp(self): @@ -158,7 +158,3 @@ def setUp(self): ) self.data = importlib.import_module(self.ZIP_MODULE) - - -class ZipSetup(ZipSetupBase): - pass From 3faf336012fa0deadb55753b52e2254dd59fb6d0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 13:25:22 -0400 Subject: [PATCH 408/496] Removed reliance on on-disk fixtures. Instead, generate on-disk and zip file fixtures on demand in temporary directories. This approach makes it easier to compose different scenarios and mix-in behavior for zip files. It also makes the disk and zip scenarios more symmetric. Re-wrote `test_reader` to utilize these new fixtures instead of assuming the files were present on disk in the test suite. --- importlib_resources/tests/data01/__init__.py | 0 importlib_resources/tests/data01/binary.file | Bin 4 -> 0 bytes .../tests/data01/subdirectory/__init__.py | 0 .../tests/data01/subdirectory/binary.file | 1 - importlib_resources/tests/data01/utf-16.file | Bin 44 -> 0 bytes importlib_resources/tests/data01/utf-8.file | 1 - importlib_resources/tests/data02/__init__.py | 0 .../tests/data02/one/__init__.py | 0 .../tests/data02/one/resource1.txt | 1 - .../subdirectory/subsubdir/resource.txt | 1 - .../tests/data02/two/__init__.py | 0 .../tests/data02/two/resource2.txt | 1 - .../tests/namespacedata01/binary.file | Bin 4 -> 0 bytes .../namespacedata01/subdirectory/binary.file | 1 - .../tests/namespacedata01/utf-16.file | Bin 44 -> 0 bytes .../tests/namespacedata01/utf-8.file | 1 - importlib_resources/tests/test_contents.py | 15 ++-- importlib_resources/tests/test_files.py | 13 +-- importlib_resources/tests/test_functional.py | 30 ++++--- importlib_resources/tests/test_open.py | 15 ++-- importlib_resources/tests/test_path.py | 5 +- importlib_resources/tests/test_read.py | 14 ++-- importlib_resources/tests/test_reader.py | 48 +++++------ importlib_resources/tests/test_resource.py | 50 ++++++------ importlib_resources/tests/util.py | 76 ++++++++++++++---- importlib_resources/tests/zip.py | 26 +++--- 26 files changed, 156 insertions(+), 143 deletions(-) delete mode 100644 importlib_resources/tests/data01/__init__.py delete mode 100644 importlib_resources/tests/data01/binary.file delete mode 100644 importlib_resources/tests/data01/subdirectory/__init__.py delete mode 100644 importlib_resources/tests/data01/subdirectory/binary.file delete mode 100644 importlib_resources/tests/data01/utf-16.file delete mode 100644 importlib_resources/tests/data01/utf-8.file delete mode 100644 importlib_resources/tests/data02/__init__.py delete mode 100644 importlib_resources/tests/data02/one/__init__.py delete mode 100644 importlib_resources/tests/data02/one/resource1.txt delete mode 100644 importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt delete mode 100644 importlib_resources/tests/data02/two/__init__.py delete mode 100644 importlib_resources/tests/data02/two/resource2.txt delete mode 100644 importlib_resources/tests/namespacedata01/binary.file delete mode 100644 importlib_resources/tests/namespacedata01/subdirectory/binary.file delete mode 100644 importlib_resources/tests/namespacedata01/utf-16.file delete mode 100644 importlib_resources/tests/namespacedata01/utf-8.file diff --git a/importlib_resources/tests/data01/__init__.py b/importlib_resources/tests/data01/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/data01/binary.file b/importlib_resources/tests/data01/binary.file deleted file mode 100644 index eaf36c1daccfdf325514461cd1a2ffbc139b5464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZQzWMT#Y01f~L diff --git a/importlib_resources/tests/data01/subdirectory/__init__.py b/importlib_resources/tests/data01/subdirectory/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/data01/subdirectory/binary.file b/importlib_resources/tests/data01/subdirectory/binary.file deleted file mode 100644 index 5bd8bb89..00000000 --- a/importlib_resources/tests/data01/subdirectory/binary.file +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/importlib_resources/tests/data01/utf-16.file b/importlib_resources/tests/data01/utf-16.file deleted file mode 100644 index 2cb772295ef4b480a8d83725bd5006a0236d8f68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44 ucmezW&x0YAAqNQa8FUyF7(y9B7~B|i84MZBfV^^`Xc15@g+Y;liva-T)Ce>H diff --git a/importlib_resources/tests/data01/utf-8.file b/importlib_resources/tests/data01/utf-8.file deleted file mode 100644 index 1c0132ad..00000000 --- a/importlib_resources/tests/data01/utf-8.file +++ /dev/null @@ -1 +0,0 @@ -Hello, UTF-8 world! diff --git a/importlib_resources/tests/data02/__init__.py b/importlib_resources/tests/data02/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/data02/one/__init__.py b/importlib_resources/tests/data02/one/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/data02/one/resource1.txt b/importlib_resources/tests/data02/one/resource1.txt deleted file mode 100644 index 61a813e4..00000000 --- a/importlib_resources/tests/data02/one/resource1.txt +++ /dev/null @@ -1 +0,0 @@ -one resource diff --git a/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt b/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt deleted file mode 100644 index 48f587a2..00000000 --- a/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt +++ /dev/null @@ -1 +0,0 @@ -a resource \ No newline at end of file diff --git a/importlib_resources/tests/data02/two/__init__.py b/importlib_resources/tests/data02/two/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_resources/tests/data02/two/resource2.txt b/importlib_resources/tests/data02/two/resource2.txt deleted file mode 100644 index a80ce46e..00000000 --- a/importlib_resources/tests/data02/two/resource2.txt +++ /dev/null @@ -1 +0,0 @@ -two resource diff --git a/importlib_resources/tests/namespacedata01/binary.file b/importlib_resources/tests/namespacedata01/binary.file deleted file mode 100644 index eaf36c1daccfdf325514461cd1a2ffbc139b5464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZQzWMT#Y01f~L diff --git a/importlib_resources/tests/namespacedata01/subdirectory/binary.file b/importlib_resources/tests/namespacedata01/subdirectory/binary.file deleted file mode 100644 index 100f5064..00000000 --- a/importlib_resources/tests/namespacedata01/subdirectory/binary.file +++ /dev/null @@ -1 +0,0 @@ -  \ No newline at end of file diff --git a/importlib_resources/tests/namespacedata01/utf-16.file b/importlib_resources/tests/namespacedata01/utf-16.file deleted file mode 100644 index 2cb772295ef4b480a8d83725bd5006a0236d8f68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44 ucmezW&x0YAAqNQa8FUyF7(y9B7~B|i84MZBfV^^`Xc15@g+Y;liva-T)Ce>H diff --git a/importlib_resources/tests/namespacedata01/utf-8.file b/importlib_resources/tests/namespacedata01/utf-8.file deleted file mode 100644 index 1c0132ad..00000000 --- a/importlib_resources/tests/namespacedata01/utf-8.file +++ /dev/null @@ -1 +0,0 @@ -Hello, UTF-8 world! diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 7dc3b0a6..741a7407 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util @@ -19,16 +18,17 @@ def test_contents(self): assert self.expected <= contents -class ContentsDiskTests(ContentsTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase): + pass class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): pass -class ContentsNamespaceTests(ContentsTests, unittest.TestCase): +class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' + expected = { # no __init__ because of namespace design 'binary.file', @@ -36,8 +36,3 @@ class ContentsNamespaceTests(ContentsTests, unittest.TestCase): 'utf-16.file', 'utf-8.file', } - - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 3e86ec64..ce0f74a7 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -6,7 +6,6 @@ import importlib_resources as resources from ..abc import Traversable -from . import data01 from . import util from . import _path from .compat.py39 import os_helper @@ -48,20 +47,16 @@ def test_old_parameter(self): resources.files(package=self.data) -class OpenDiskTests(FilesTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase): + pass class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): pass -class OpenNamespaceTests(FilesTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 +class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 69706cf7..9ec717f2 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -1,6 +1,7 @@ import unittest import os import contextlib +import importlib try: from test.support.warnings_helper import ignore_warnings, check_warnings @@ -10,22 +11,33 @@ import importlib_resources as resources +from . import util + # Since the functional API forwards to Traversable, we only test # filesystem resources here -- not zip files, namespace packages etc. # We do test for two kinds of Anchor, though. class StringAnchorMixin: - anchor01 = 'importlib_resources.tests.data01' - anchor02 = 'importlib_resources.tests.data02' + anchor01 = 'data01' + anchor02 = 'data02' class ModuleAnchorMixin: - from . import data01 as anchor01 - from . import data02 as anchor02 + @property + def anchor01(self): + return importlib.import_module('data01') + + @property + def anchor02(self): + return importlib.import_module('data02') + +class FunctionalAPIBase(util.DiskSetup): + def setUp(self): + super().setUp() + self.load_fixture('data02') -class FunctionalAPIBase: def _gen_resourcetxt_path_parts(self): """Yield various names of a text file in anchor02, each in a subTest""" for path_parts in ( @@ -227,16 +239,16 @@ def test_text_errors(self): class FunctionalAPITest_StringAnchor( - unittest.TestCase, - FunctionalAPIBase, StringAnchorMixin, + FunctionalAPIBase, + unittest.TestCase, ): pass class FunctionalAPITest_ModuleAnchor( - unittest.TestCase, - FunctionalAPIBase, ModuleAnchorMixin, + FunctionalAPIBase, + unittest.TestCase, ): pass diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 44f1018a..c40bb8c6 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util @@ -65,16 +64,12 @@ def test_open_text_FileNotFoundError(self): target.open(encoding='utf-8') -class OpenDiskTests(OpenTests, unittest.TestCase): - def setUp(self): - self.data = data01 - +class OpenDiskTests(OpenTests, util.DiskSetup, unittest.TestCase): + pass -class OpenDiskNamespaceTests(OpenTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 - self.data = namespacedata01 +class OpenDiskNamespaceTests(OpenTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): @@ -82,7 +77,7 @@ class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' if __name__ == '__main__': diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index c3e1cbb4..1e30f2bc 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -3,7 +3,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util @@ -25,9 +24,7 @@ def test_reading(self): self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) -class PathDiskTests(PathTests, unittest.TestCase): - data = data01 - +class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase): def test_natural_path(self): """ Guarantee the internal implementation detail that diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 97d90128..6780a2d1 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -1,7 +1,6 @@ import unittest import importlib_resources as resources -from . import data01 from . import util from importlib import import_module @@ -52,8 +51,8 @@ def test_read_text_with_errors(self): ) -class ReadDiskTests(ReadTests, unittest.TestCase): - data = data01 +class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase): + pass class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): @@ -69,15 +68,12 @@ def test_read_submodule_resource_by_name(self): self.assertEqual(result, bytes(range(4, 8))) -class ReadNamespaceTests(ReadTests, unittest.TestCase): - def setUp(self): - from . import namespacedata01 - - self.data = namespacedata01 +class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' def test_read_submodule_resource(self): submodule = import_module('namespacedata01.subdirectory') diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 95c2fc85..0a77eb40 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -1,16 +1,21 @@ import os.path -import sys import pathlib import unittest from importlib import import_module from importlib_resources.readers import MultiplexedPath, NamespaceReader +from . import util -class MultiplexedPathTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.folder = pathlib.Path(__file__).parent / 'namespacedata01' + +class MultiplexedPathTest(util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' + + def setUp(self): + super().setUp() + self.folder = pathlib.Path(self.data.__path__[0]) + self.data01 = pathlib.Path(self.load_fixture('data01').__file__).parent + self.data02 = pathlib.Path(self.load_fixture('data02').__file__).parent def test_init_no_paths(self): with self.assertRaises(FileNotFoundError): @@ -31,9 +36,8 @@ def test_iterdir(self): ) def test_iterdir_duplicate(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') contents = { - path.name for path in MultiplexedPath(self.folder, data01).iterdir() + path.name for path in MultiplexedPath(self.folder, self.data01).iterdir() } for remove in ('__pycache__', '__init__.pyc'): try: @@ -61,9 +65,8 @@ def test_open_file(self): path.open() def test_join_path(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') - prefix = str(data01.parent) - path = MultiplexedPath(self.folder, data01) + prefix = str(self.folder.parent) + path = MultiplexedPath(self.folder, self.data01) self.assertEqual( str(path.joinpath('binary.file'))[len(prefix) + 1 :], os.path.join('namespacedata01', 'binary.file'), @@ -83,10 +86,8 @@ def test_join_path_compound(self): assert not path.joinpath('imaginary/foo.py').exists() def test_join_path_common_subdir(self): - data01 = pathlib.Path(__file__).parent.joinpath('data01') - data02 = pathlib.Path(__file__).parent.joinpath('data02') - prefix = str(data01.parent) - path = MultiplexedPath(data01, data02) + prefix = str(self.data02.parent) + path = MultiplexedPath(self.data01, self.data02) self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) self.assertEqual( str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :], @@ -106,16 +107,8 @@ def test_name(self): ) -class NamespaceReaderTest(unittest.TestCase): - site_dir = str(pathlib.Path(__file__).parent) - - @classmethod - def setUpClass(cls): - sys.path.append(cls.site_dir) - - @classmethod - def tearDownClass(cls): - sys.path.remove(cls.site_dir) +class NamespaceReaderTest(util.DiskSetup, unittest.TestCase): + MODULE = 'namespacedata01' def test_init_error(self): with self.assertRaises(ValueError): @@ -125,7 +118,7 @@ def test_resource_path(self): namespacedata01 = import_module('namespacedata01') reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) - root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + root = self.data.__path__[0] self.assertEqual( reader.resource_path('binary.file'), os.path.join(root, 'binary.file') ) @@ -134,9 +127,8 @@ def test_resource_path(self): ) def test_files(self): - namespacedata01 = import_module('namespacedata01') - reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) - root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + reader = NamespaceReader(self.data.__spec__.submodule_search_locations) + root = self.data.__path__[0] self.assertIsInstance(reader.files(), MultiplexedPath) self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 32aa909a..a0da6a35 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -1,9 +1,6 @@ -import sys import unittest import importlib_resources as resources -import pathlib -from . import data01 from . import util from importlib import import_module @@ -25,9 +22,8 @@ def test_is_dir(self): self.assertTrue(target.is_dir()) -class ResourceDiskTests(ResourceTests, unittest.TestCase): - def setUp(self): - self.data = data01 +class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase): + pass class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): @@ -38,33 +34,39 @@ def names(traversable): return {item.name for item in traversable.iterdir()} -class ResourceLoaderTests(unittest.TestCase): +class ResourceLoaderTests(util.DiskSetup, unittest.TestCase): def test_resource_contents(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + file=self.data, path=self.data.__file__, contents=['A', 'B', 'C'] ) self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) def test_is_file(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertTrue(resources.files(package).joinpath('B').is_file()) def test_is_dir(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertTrue(resources.files(package).joinpath('D').is_dir()) def test_resource_missing(self): package = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + file=self.data, + path=self.data.__file__, + contents=['A', 'B', 'C', 'D/E', 'D/F'], ) self.assertFalse(resources.files(package).joinpath('Z').is_file()) -class ResourceCornerCaseTests(unittest.TestCase): +class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase): def test_package_has_no_reader_fallback(self): """ Test odd ball packages which: @@ -73,7 +75,7 @@ def test_package_has_no_reader_fallback(self): # 3. Are not in a zip file """ module = util.create_package( - file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + file=self.data, path=self.data.__file__, contents=['A', 'B', 'C'] ) # Give the module a dummy loader. module.__loader__ = object() @@ -85,8 +87,6 @@ def test_package_has_no_reader_fallback(self): class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'data01' - def test_is_submodule_resource(self): submodule = import_module('data01.subdirectory') self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) @@ -118,7 +118,7 @@ def test_as_file_directory(self): class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase): - ZIP_MODULE = 'data02' + MODULE = 'data02' def test_unrelated_contents(self): """ @@ -217,16 +217,12 @@ def test_submodule_sub_contents_by_name(self): self.assertEqual(contents, {'binary.file'}) -class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase): - site_dir = str(pathlib.Path(__file__).parent) - - @classmethod - def setUpClass(cls): - sys.path.append(cls.site_dir) - - @classmethod - def tearDownClass(cls): - sys.path.remove(cls.site_dir) +class ResourceFromNamespaceDiskTests( + util.DiskSetup, + ResourceFromNamespaceTests, + unittest.TestCase, +): + MODULE = 'namespacedata01' class ResourceFromNamespaceZipTests( @@ -234,7 +230,7 @@ class ResourceFromNamespaceZipTests( ResourceFromNamespaceTests, unittest.TestCase, ): - ZIP_MODULE = 'namespacedata01' + MODULE = 'namespacedata01' if __name__ == '__main__': diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index b258aec0..6b901f67 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -6,10 +6,10 @@ import pathlib import contextlib -from . import data01 from ..abc import ResourceReader from .compat.py39 import import_helper, os_helper from . import zip as zip_ +from . import _path from importlib.machinery import ModuleSpec @@ -68,7 +68,7 @@ def create_package(file=None, path=None, is_package=True, contents=()): ) -class CommonTests(metaclass=abc.ABCMeta): +class CommonTestsBase(metaclass=abc.ABCMeta): """ Tests shared by test_open, test_path, and test_read. """ @@ -84,34 +84,34 @@ def test_package_name(self): """ Passing in the package name should succeed. """ - self.execute(data01.__name__, 'utf-8.file') + self.execute(self.data.__name__, 'utf-8.file') def test_package_object(self): """ Passing in the package itself should succeed. """ - self.execute(data01, 'utf-8.file') + self.execute(self.data, 'utf-8.file') def test_string_path(self): """ Passing in a string for the path should succeed. """ path = 'utf-8.file' - self.execute(data01, path) + self.execute(self.data, path) def test_pathlib_path(self): """ Passing in a pathlib.PurePath object for the path should succeed. """ path = pathlib.PurePath('utf-8.file') - self.execute(data01, path) + self.execute(self.data, path) def test_importing_module_as_side_effect(self): """ The anchor package can already be imported. """ - del sys.modules[data01.__name__] - self.execute(data01.__name__, 'utf-8.file') + del sys.modules[self.data.__name__] + self.execute(self.data.__name__, 'utf-8.file') def test_missing_path(self): """ @@ -141,20 +141,68 @@ def test_useless_loader(self): self.execute(package, 'utf-8.file') -class ZipSetup: - ZIP_MODULE = 'data01' - +fixtures = dict( + data01={ + '__init__.py': '', + 'binary.file': bytes(range(4)), + 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), + 'subdirectory': { + '__init__.py': '', + 'binary.file': bytes(range(4, 8)), + }, + }, + data02={ + '__init__.py': '', + 'one': {'__init__.py': '', 'resource1.txt': 'one resource'}, + 'two': {'__init__.py': '', 'resource2.txt': 'two resource'}, + 'subdirectory': {'subsubdir': {'resource.txt': 'a resource'}}, + }, + namespacedata01={ + 'binary.file': bytes(range(4)), + 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), + 'subdirectory': { + 'binary.file': bytes(range(12, 16)), + }, + }, +) + + +class ModuleSetup: def setUp(self): self.fixtures = contextlib.ExitStack() self.addCleanup(self.fixtures.close) self.fixtures.enter_context(import_helper.isolated_modules()) + self.data = self.load_fixture(self.MODULE) + + +class ZipSetup(ModuleSetup): + MODULE = 'data01' + def load_fixture(self, module): temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) modules = pathlib.Path(temp_dir) / 'zipped modules.zip' - src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE) self.fixtures.enter_context( - import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules))) + import_helper.DirsOnSysPath( + str(zip_.make_zip_file({module: fixtures[module]}, modules)) + ) ) - self.data = importlib.import_module(self.ZIP_MODULE) + return importlib.import_module(module) + + +class DiskSetup(ModuleSetup): + MODULE = 'data01' + + def load_fixture(self, module): + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + _path.build({module: fixtures[module]}, pathlib.Path(temp_dir)) + self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir)) + + return importlib.import_module(module) + + +class CommonTests(DiskSetup, CommonTestsBase): + pass diff --git a/importlib_resources/tests/zip.py b/importlib_resources/tests/zip.py index 962195a9..51ee5648 100755 --- a/importlib_resources/tests/zip.py +++ b/importlib_resources/tests/zip.py @@ -2,31 +2,25 @@ Generate zip test data files. """ -import contextlib -import os -import pathlib import zipfile import zipp -def make_zip_file(src, dst): +def make_zip_file(tree, dst): """ - Zip the files in src into a new zipfile at dst. + Zip the files in tree into a new zipfile at dst. """ with zipfile.ZipFile(dst, 'w') as zf: - for src_path, rel in walk(src): - dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) - zf.write(src_path, dst_name) + for name, contents in walk(tree): + zf.writestr(name, contents) zipp.CompleteDirs.inject(zf) return dst -def walk(datapath): - for dirpath, dirnames, filenames in os.walk(datapath): - with contextlib.suppress(ValueError): - dirnames.remove('__pycache__') - for filename in filenames: - res = pathlib.Path(dirpath) / filename - rel = res.relative_to(datapath) - yield res, rel +def walk(tree, prefix=''): + for name, contents in tree.items(): + if isinstance(contents, dict): + yield from walk(contents, prefix=f'{prefix}{name}/') + else: + yield f'{prefix}{name}', contents From 0255da2f880ccb5882f0cbcdc1f55b133b94869b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:24:33 -0400 Subject: [PATCH 409/496] Refactor test setup to allow overriding of load_fixture with re-use of tree_on_path. --- importlib_resources/tests/util.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 6b901f67..a4eafac3 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -177,32 +177,30 @@ def setUp(self): self.fixtures.enter_context(import_helper.isolated_modules()) self.data = self.load_fixture(self.MODULE) + def load_fixture(self, module): + self.tree_on_path({module: fixtures[module]}) + return importlib.import_module(module) + class ZipSetup(ModuleSetup): MODULE = 'data01' - def load_fixture(self, module): + def tree_on_path(self, spec): temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) modules = pathlib.Path(temp_dir) / 'zipped modules.zip' self.fixtures.enter_context( - import_helper.DirsOnSysPath( - str(zip_.make_zip_file({module: fixtures[module]}, modules)) - ) + import_helper.DirsOnSysPath(str(zip_.make_zip_file(spec, modules))) ) - return importlib.import_module(module) - class DiskSetup(ModuleSetup): MODULE = 'data01' - def load_fixture(self, module): + def tree_on_path(self, spec): temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) - _path.build({module: fixtures[module]}, pathlib.Path(temp_dir)) + _path.build(spec, pathlib.Path(temp_dir)) self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir)) - return importlib.import_module(module) - class CommonTests(DiskSetup, CommonTestsBase): pass From abaac82155d82ad88cc3024b42c051b89ab09be6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:30:36 -0400 Subject: [PATCH 410/496] Refactor ModuleFiles tests to make room for also testing zip files. --- importlib_resources/tests/test_files.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index ce0f74a7..2514d4ac 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -72,20 +72,28 @@ def setUp(self): self.fixtures.enter_context(import_helper.isolated_modules()) -class ModulesFilesTests(SiteDir, unittest.TestCase): +class ModulesFiles: + spec = { + 'mod.py': '', + 'res.txt': 'resources are the best', + } + MODULE = 'unused' + def test_module_resources(self): """ A module can have resources found adjacent to the module. """ - spec = { - 'mod.py': '', - 'res.txt': 'resources are the best', - } - _path.build(spec, self.site_dir) import mod actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') - assert actual == spec['res.txt'] + assert actual == self.spec['res.txt'] + + def load_fixture(self, name): + self.tree_on_path(self.spec) + + +class ModuleFilesDiskTests(ModulesFiles, util.DiskSetup, unittest.TestCase): + pass class ImplicitContextFilesTests(SiteDir, unittest.TestCase): From 114db55514326bf602543f5812da6430731be6ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:35:49 -0400 Subject: [PATCH 411/496] Refactor ImplicitContextFiles tests to leverage the same DirectSpec. --- importlib_resources/tests/test_files.py | 50 +++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 2514d4ac..85a5f8f2 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -7,7 +7,6 @@ import importlib_resources as resources from ..abc import Traversable from . import util -from . import _path from .compat.py39 import os_helper from .compat.py312 import import_helper @@ -72,12 +71,22 @@ def setUp(self): self.fixtures.enter_context(import_helper.isolated_modules()) +class DirectSpec: + """ + Override behavior of ModuleSetup to write a full spec directly. + """ + + MODULE = 'unused' + + def load_fixture(self, name): + self.tree_on_path(self.spec) + + class ModulesFiles: spec = { 'mod.py': '', 'res.txt': 'resources are the best', } - MODULE = 'unused' def test_module_resources(self): """ @@ -88,33 +97,36 @@ def test_module_resources(self): actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') assert actual == self.spec['res.txt'] - def load_fixture(self, name): - self.tree_on_path(self.spec) - -class ModuleFilesDiskTests(ModulesFiles, util.DiskSetup, unittest.TestCase): +class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase): pass -class ImplicitContextFilesTests(SiteDir, unittest.TestCase): +class ImplicitContextFiles: + spec = { + 'somepkg': { + '__init__.py': textwrap.dedent( + """ + import importlib_resources as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ), + 'res.txt': 'resources are the best', + }, + } + def test_implicit_files(self): """ Without any parameter, files() will infer the location as the caller. """ - spec = { - 'somepkg': { - '__init__.py': textwrap.dedent( - """ - import importlib_resources as res - val = res.files().joinpath('res.txt').read_text(encoding='utf-8') - """ - ), - 'res.txt': 'resources are the best', - }, - } - _path.build(spec, self.site_dir) assert importlib.import_module('somepkg').val == 'resources are the best' +class ImplicitContextFilesDiskTests( + DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase +): + pass + + if __name__ == '__main__': unittest.main() From d873fcadff8f5423aad746b4d7780c4e9957d431 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:36:13 -0400 Subject: [PATCH 412/496] Remove SiteDir, no longer needed. --- importlib_resources/tests/test_files.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 85a5f8f2..70316e07 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -7,8 +7,6 @@ import importlib_resources as resources from ..abc import Traversable from . import util -from .compat.py39 import os_helper -from .compat.py312 import import_helper @contextlib.contextmanager @@ -62,15 +60,6 @@ class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): ZIP_MODULE = 'namespacedata01' -class SiteDir: - def setUp(self): - self.fixtures = contextlib.ExitStack() - self.addCleanup(self.fixtures.close) - self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) - self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) - self.fixtures.enter_context(import_helper.isolated_modules()) - - class DirectSpec: """ Override behavior of ModuleSetup to write a full spec directly. From 34ed4f731077b5011ee4f7dfe0be6c0df7a07efc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:42:57 -0400 Subject: [PATCH 413/496] Expand ModuleFiles and ImplicitContextFiles tests to include zip files. Module files zip tests is marked as xfail due to the issue reported in python/cpython#121735. --- importlib_resources/tests/test_files.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 70316e07..61f901e3 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,6 +4,8 @@ import importlib import contextlib +import pytest + import importlib_resources as resources from ..abc import Traversable from . import util @@ -91,6 +93,11 @@ class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.Te pass +@pytest.mark.xfail(reason="python/cpython#121735") +class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase): + pass + + class ImplicitContextFiles: spec = { 'somepkg': { @@ -117,5 +124,11 @@ class ImplicitContextFilesDiskTests( pass +class ImplicitContextFilesZipTests( + DirectSpec, util.ZipSetup, ImplicitContextFiles, unittest.TestCase +): + pass + + if __name__ == '__main__': unittest.main() From 8ba5553b95058bb2776c5b4b3e8f245d187e09cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 15:50:28 -0400 Subject: [PATCH 414/496] Add another test in implicit files that loads the resource from a submodule. --- importlib_resources/tests/test_files.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 61f901e3..ef2e9416 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -99,24 +99,33 @@ class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.Test class ImplicitContextFiles: + set_val = textwrap.dedent( + """ + import importlib_resources as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ) spec = { 'somepkg': { - '__init__.py': textwrap.dedent( - """ - import importlib_resources as res - val = res.files().joinpath('res.txt').read_text(encoding='utf-8') - """ - ), + '__init__.py': set_val, + 'submod.py': set_val, 'res.txt': 'resources are the best', }, } - def test_implicit_files(self): + def test_implicit_files_package(self): """ Without any parameter, files() will infer the location as the caller. """ assert importlib.import_module('somepkg').val == 'resources are the best' + @pytest.mark.xfail(reason="python/cpython#121735") + def test_implicit_files_submodule(self): + """ + Without any parameter, files() will infer the location as the caller. + """ + assert importlib.import_module('somepkg.submod').val == 'resources are the best' + class ImplicitContextFilesDiskTests( DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase From 23afc90b7fce9dffc9139407dcac4489f4164cd0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 16:10:24 -0400 Subject: [PATCH 415/496] When constructing ZipReader, only append the name if the indicated module is a package. Fixes python/cpython#121735. --- importlib_resources/readers.py | 6 ++++-- importlib_resources/tests/test_files.py | 4 ---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 4a80a774..abfcde73 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -34,8 +34,10 @@ def files(self): class ZipReader(abc.TraversableResources): def __init__(self, loader, module): - _, _, name = module.rpartition('.') - self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.prefix = loader.prefix.replace('\\', '/') + if loader.is_package(module): + _, _, name = module.rpartition('.') + self.prefix += name + '/' self.archive = loader.archive def open_resource(self, resource): diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index ef2e9416..564b2ccf 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -4,8 +4,6 @@ import importlib import contextlib -import pytest - import importlib_resources as resources from ..abc import Traversable from . import util @@ -93,7 +91,6 @@ class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.Te pass -@pytest.mark.xfail(reason="python/cpython#121735") class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase): pass @@ -119,7 +116,6 @@ def test_implicit_files_package(self): """ assert importlib.import_module('somepkg').val == 'resources are the best' - @pytest.mark.xfail(reason="python/cpython#121735") def test_implicit_files_submodule(self): """ Without any parameter, files() will infer the location as the caller. From 796c028dc4a5fede396624508a1031fae16fde3a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 16:11:10 -0400 Subject: [PATCH 416/496] Add news fragment. --- newsfragments/+4c8e90fb.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+4c8e90fb.bugfix.rst diff --git a/newsfragments/+4c8e90fb.bugfix.rst b/newsfragments/+4c8e90fb.bugfix.rst new file mode 100644 index 00000000..6faf6130 --- /dev/null +++ b/newsfragments/+4c8e90fb.bugfix.rst @@ -0,0 +1 @@ +When constructing ZipReader, only append the name if the indicated module is a package. (python/cpython#121735) From 0fcdaab3ee770613d9fecbec6f2e91c301631acb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 16:13:08 -0400 Subject: [PATCH 417/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+4c8e90fb.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+4c8e90fb.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index c4a6f36c..f5cd7e60 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.4.1 +====== + +Bugfixes +-------- + +- When constructing ZipReader, only append the name if the indicated module is a package. (python/cpython#121735) + + v6.4.0 ====== diff --git a/newsfragments/+4c8e90fb.bugfix.rst b/newsfragments/+4c8e90fb.bugfix.rst deleted file mode 100644 index 6faf6130..00000000 --- a/newsfragments/+4c8e90fb.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -When constructing ZipReader, only append the name if the indicated module is a package. (python/cpython#121735) From 8c274e5e6663eb0eafecb54c47f951337a9859ed Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 5 Apr 2024 17:00:29 +0200 Subject: [PATCH 418/496] gh-116608: Ignore UTF-16 BOM in importlib.resources._functional tests (GH-117569) gh-116609: Ignore UTF-16 BOM in importlib.resources._functional tests To test the `errors` argument, we read a UTF-16 file as UTF-8 with "backslashreplace" error handling. However, the utf-16 codec adds an endian-specific byte-order mark, so on big-endian machines the expectation doesn't match the test file (which was saved on a little-endian machine). Use endswith to ignore the BOM. --- importlib_resources/tests/test_functional.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 69706cf7..11a45969 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -36,6 +36,12 @@ def _gen_resourcetxt_path_parts(self): with self.subTest(path_parts=path_parts): yield path_parts + def assertEndsWith(self, string, suffix): + """Assert that `string` ends with `suffix`. + + Used to ignore an architecture-specific UTF-16 byte-order mark.""" + self.assertEqual(string[-len(suffix):], suffix) + def test_read_text(self): self.assertEqual( resources.read_text(self.anchor01, 'utf-8.file'), @@ -76,13 +82,13 @@ def test_read_text(self): ), '\x00\x01\x02\x03', ) - self.assertEqual( + self.assertEndsWith( # ignore the BOM resources.read_text( self.anchor01, 'utf-16.file', errors='backslashreplace', ), - 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( errors='backslashreplace', ), ) @@ -128,9 +134,9 @@ def test_open_text(self): 'utf-16.file', errors='backslashreplace', ) as f: - self.assertEqual( + self.assertEndsWith( # ignore the BOM f.read(), - 'Hello, UTF-16 world!\n'.encode('utf-16').decode( + 'Hello, UTF-16 world!\n'.encode('utf-16-le').decode( errors='backslashreplace', ), ) From b44af23d1734a881c3a065b39d5bc0187154ce7b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 16:54:38 -0400 Subject: [PATCH 419/496] =?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 --- importlib_resources/tests/test_functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 11a45969..51dd52a3 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -40,7 +40,7 @@ def assertEndsWith(self, string, suffix): """Assert that `string` ends with `suffix`. Used to ignore an architecture-specific UTF-16 byte-order mark.""" - self.assertEqual(string[-len(suffix):], suffix) + self.assertEqual(string[-len(suffix) :], suffix) def test_read_text(self): self.assertEqual( From 509cabd46b54f5f6e372d923039c2bff8fff7d94 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 16:57:33 -0400 Subject: [PATCH 420/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/312.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/312.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index f5cd7e60..020292e8 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.4.2 +====== + +Bugfixes +-------- + +- Merged fix for UTF-16 BOM handling in functional tests. (#312) + + v6.4.1 ====== diff --git a/newsfragments/312.bugfix.rst b/newsfragments/312.bugfix.rst deleted file mode 100644 index 23eba650..00000000 --- a/newsfragments/312.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Merged fix for UTF-16 BOM handling in functional tests. \ No newline at end of file From 31de3ca4bd16d0731b1d20c08298a673f170c2a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 17:00:15 -0400 Subject: [PATCH 421/496] Rename module to match downstream. --- importlib_resources/__init__.py | 2 +- importlib_resources/{functional.py => _functional.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename importlib_resources/{functional.py => _functional.py} (100%) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 0d029abd..ec4441c9 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -7,7 +7,7 @@ Anchor, ) -from .functional import ( +from ._functional import ( contents, is_resource, open_binary, diff --git a/importlib_resources/functional.py b/importlib_resources/_functional.py similarity index 100% rename from importlib_resources/functional.py rename to importlib_resources/_functional.py From fb44cbf6d5db08b73bed334a9ced890773d033b3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 17:15:46 -0400 Subject: [PATCH 422/496] Extract compatibility import for warnings helper. --- importlib_resources/tests/compat/py39.py | 3 +++ importlib_resources/tests/test_functional.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/importlib_resources/tests/compat/py39.py b/importlib_resources/tests/compat/py39.py index e158eb85..e01d276b 100644 --- a/importlib_resources/tests/compat/py39.py +++ b/importlib_resources/tests/compat/py39.py @@ -8,3 +8,6 @@ 'modules_setup', 'modules_cleanup', 'DirsOnSysPath' ) os_helper = try_import('os_helper') or from_test_support('temp_dir') +warnings_helper = try_import('warnings_helper') or from_test_support( + 'ignore_warnings', 'check_warnings' +) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 51dd52a3..3f62bd40 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -2,11 +2,7 @@ import os import contextlib -try: - from test.support.warnings_helper import ignore_warnings, check_warnings -except ImportError: - # older Python versions - from test.support import ignore_warnings, check_warnings +from .compat.py39 import warnings_helper import importlib_resources as resources @@ -169,7 +165,7 @@ def test_is_resource(self): self.assertTrue(is_resource(self.anchor02, *path_parts)) def test_contents(self): - with check_warnings((".*contents.*", DeprecationWarning)): + with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01) self.assertGreaterEqual( set(c), @@ -177,24 +173,28 @@ def test_contents(self): ) with contextlib.ExitStack() as cm: cm.enter_context(self.assertRaises(OSError)) - cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) + cm.enter_context( + warnings_helper.check_warnings((".*contents.*", DeprecationWarning)) + ) list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): with contextlib.ExitStack() as cm: cm.enter_context(self.assertRaises(OSError)) - cm.enter_context(check_warnings((".*contents.*", DeprecationWarning))) + cm.enter_context( + warnings_helper.check_warnings((".*contents.*", DeprecationWarning)) + ) list(resources.contents(self.anchor01, *path_parts)) - with check_warnings((".*contents.*", DeprecationWarning)): + with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') self.assertGreaterEqual( set(c), {'binary.file'}, ) - @ignore_warnings(category=DeprecationWarning) + @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_common_errors(self): for func in ( resources.read_text, From 81ddbef7bd95aef5eeb58a26000fe05e9276b8e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Aug 2024 17:33:23 -0400 Subject: [PATCH 423/496] Use a syntax compatible with Python 3.8 for join context. --- importlib_resources/tests/test_functional.py | 21 ++++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 3f62bd40..adedd01b 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -1,6 +1,5 @@ import unittest import os -import contextlib from .compat.py39 import warnings_helper @@ -171,21 +170,17 @@ def test_contents(self): set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with contextlib.ExitStack() as cm: - cm.enter_context(self.assertRaises(OSError)) - cm.enter_context( - warnings_helper.check_warnings((".*contents.*", DeprecationWarning)) - ) - + with self.assertRaises(OSError), warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )): list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): - with contextlib.ExitStack() as cm: - cm.enter_context(self.assertRaises(OSError)) - cm.enter_context( - warnings_helper.check_warnings((".*contents.*", DeprecationWarning)) - ) - + with self.assertRaises(OSError), warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )): list(resources.contents(self.anchor01, *path_parts)) with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') From 198adec064edf0c10aac45ab85c68535ba038a59 Mon Sep 17 00:00:00 2001 From: Gatsik <74517072+Gatsik@users.noreply.github.com> Date: Sat, 17 Aug 2024 00:59:03 +0300 Subject: [PATCH 424/496] gh-121735: Fix inferring caller when resolving importlib.resources.files() without anchor --- importlib_resources/_common.py | 7 +++++- importlib_resources/tests/test_files.py | 31 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 8df6b39e..970585e3 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -91,9 +91,14 @@ def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ + this_frame = inspect.currentframe() + if this_frame is None: + this_file = __file__ + else: + this_file = inspect.getframeinfo(this_frame).filename def is_this_file(frame_info): - return frame_info.filename == __file__ + return frame_info.filename == this_file def is_wrapper(frame_info): return frame_info.function == 'wrapper' diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 564b2ccf..01b279a5 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,3 +1,7 @@ +import os +import pathlib +import py_compile +import shutil import textwrap import unittest import warnings @@ -122,6 +126,33 @@ def test_implicit_files_submodule(self): """ assert importlib.import_module('somepkg.submod').val == 'resources are the best' + def _compile_importlib(self, target_dir): + importlib_dir = pathlib.Path(importlib.__file__).parent + shutil.copytree(importlib_dir, target_dir, ignore=lambda *_: ['__pycache__']) + + for dirpath, _, filenames in os.walk(target_dir): + for filename in filenames: + source_path = pathlib.Path(dirpath) / filename + cfile = source_path.with_suffix('.pyc') + py_compile.compile(source_path, cfile) + pathlib.Path.unlink(source_path) + + def test_implicit_files_with_compiled_importlib(self): + self._compile_importlib(pathlib.Path(self.site_dir) / 'cimportlib') + spec = { + 'somepkg': { + '__init__.py': textwrap.dedent( + """ + import cimportlib.resources as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ), + 'res.txt': 'resources are the best', + }, + } + _path.build(spec, self.site_dir) + assert importlib.import_module('somepkg').val == 'resources are the best' + class ImplicitContextFilesDiskTests( DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase From cba8dce7839977c66806ef05e122cf38ed14a113 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Aug 2024 22:27:46 -0400 Subject: [PATCH 425/496] Adapt changes for new fixtures. --- importlib_resources/tests/test_files.py | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 01b279a5..23b4ad82 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -11,6 +11,7 @@ import importlib_resources as resources from ..abc import Traversable from . import util +from .compat.py39 import os_helper, import_helper @contextlib.contextmanager @@ -112,6 +113,10 @@ class ImplicitContextFiles: 'submod.py': set_val, 'res.txt': 'resources are the best', }, + 'frozenpkg': { + '__init__.py': set_val.replace('importlib_resources', 'c_resources'), + 'res.txt': 'resources are the best', + }, } def test_implicit_files_package(self): @@ -126,32 +131,26 @@ def test_implicit_files_submodule(self): """ assert importlib.import_module('somepkg.submod').val == 'resources are the best' - def _compile_importlib(self, target_dir): - importlib_dir = pathlib.Path(importlib.__file__).parent - shutil.copytree(importlib_dir, target_dir, ignore=lambda *_: ['__pycache__']) + def _compile_importlib(self): + """ + Make a compiled-only copy of the importlib resources package. + """ + bin_site = self.fixtures.enter_context(os_helper.temp_dir()) + c_resources = pathlib.Path(bin_site, 'c_resources') + sources = pathlib.Path(resources.__file__).parent + shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__']) - for dirpath, _, filenames in os.walk(target_dir): + for dirpath, _, filenames in os.walk(c_resources): for filename in filenames: source_path = pathlib.Path(dirpath) / filename cfile = source_path.with_suffix('.pyc') py_compile.compile(source_path, cfile) pathlib.Path.unlink(source_path) + self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): - self._compile_importlib(pathlib.Path(self.site_dir) / 'cimportlib') - spec = { - 'somepkg': { - '__init__.py': textwrap.dedent( - """ - import cimportlib.resources as res - val = res.files().joinpath('res.txt').read_text(encoding='utf-8') - """ - ), - 'res.txt': 'resources are the best', - }, - } - _path.build(spec, self.site_dir) - assert importlib.import_module('somepkg').val == 'resources are the best' + self._compile_importlib() + assert importlib.import_module('frozenpkg').val == 'resources are the best' class ImplicitContextFilesDiskTests( From 4ea81bf920f6cc6377ccc7fbc1f4f343927f4f20 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Aug 2024 23:04:07 -0400 Subject: [PATCH 426/496] Extract a function for computing 'this filename' once. --- importlib_resources/_common.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 970585e3..364e4c0f 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -87,18 +87,19 @@ def _(cand: None) -> types.ModuleType: return resolve(_infer_caller().f_globals['__name__']) +@functools.lru_cache +def _this_filename(): + frame = inspect.currentframe() + return __file__ if frame is None else inspect.getframeinfo(frame).filename + + def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ - this_frame = inspect.currentframe() - if this_frame is None: - this_file = __file__ - else: - this_file = inspect.getframeinfo(this_frame).filename def is_this_file(frame_info): - return frame_info.filename == this_file + return frame_info.filename == _this_filename() def is_wrapper(frame_info): return frame_info.function == 'wrapper' From ebc5b97ffe1d20eb6ebc536dd1f6b385f83652a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:04:42 -0400 Subject: [PATCH 427/496] Extract the filename from the topmost frame of the stack. --- importlib_resources/_common.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 364e4c0f..e95371cd 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -87,24 +87,19 @@ def _(cand: None) -> types.ModuleType: return resolve(_infer_caller().f_globals['__name__']) -@functools.lru_cache -def _this_filename(): - frame = inspect.currentframe() - return __file__ if frame is None else inspect.getframeinfo(frame).filename - - def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ def is_this_file(frame_info): - return frame_info.filename == _this_filename() + return frame_info.filename == stack[0].filename def is_wrapper(frame_info): return frame_info.function == 'wrapper' - not_this_file = itertools.filterfalse(is_this_file, inspect.stack()) + stack = inspect.stack() + not_this_file = itertools.filterfalse(is_this_file, stack) # also exclude 'wrapper' due to singledispatch in the call stack callers = itertools.filterfalse(is_wrapper, not_this_file) return next(callers).frame From d618902dbe0e9f94e634a95bc9bbaf941236fc0c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:01:09 -0400 Subject: [PATCH 428/496] Add news fragment. --- newsfragments/+0f77c990.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+0f77c990.bugfix.rst diff --git a/newsfragments/+0f77c990.bugfix.rst b/newsfragments/+0f77c990.bugfix.rst new file mode 100644 index 00000000..99c0dfca --- /dev/null +++ b/newsfragments/+0f77c990.bugfix.rst @@ -0,0 +1 @@ +When inferring the caller in ``files()`` correctly detect one's own module even when the resources package source is not present. (python/cpython#123085) \ No newline at end of file From 90c0e420ef15256f116342c97ea984a2fa604cc3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:08:13 -0400 Subject: [PATCH 429/496] Rely on `resources.__name__` for easier portability. --- importlib_resources/tests/test_files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 23b4ad82..874874fb 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -102,8 +102,8 @@ class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.Test class ImplicitContextFiles: set_val = textwrap.dedent( - """ - import importlib_resources as res + f""" + import {resources.__name__} as res val = res.files().joinpath('res.txt').read_text(encoding='utf-8') """ ) @@ -114,7 +114,7 @@ class ImplicitContextFiles: 'res.txt': 'resources are the best', }, 'frozenpkg': { - '__init__.py': set_val.replace('importlib_resources', 'c_resources'), + '__init__.py': set_val.replace(resources.__name__, 'c_resources'), 'res.txt': 'resources are the best', }, } From 79fa62f4b5cbf8f358560651a714b282aee2226c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:09:58 -0400 Subject: [PATCH 430/496] Add docstring and reference to the issue. --- importlib_resources/tests/test_files.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 874874fb..9cb55d06 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -149,6 +149,11 @@ def _compile_importlib(self): self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): + """ + Caller detection works for compiled-only resources module. + + python/cpython#123085 + """ self._compile_importlib() assert importlib.import_module('frozenpkg').val == 'resources are the best' From d02141768b62468e46064614276036ea5c746056 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:11:32 -0400 Subject: [PATCH 431/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+0f77c990.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+0f77c990.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 020292e8..5f1ab11e 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.4.3 +====== + +Bugfixes +-------- + +- When inferring the caller in ``files()`` correctly detect one's own module even when the resources package source is not present. (python/cpython#123085) + + v6.4.2 ====== diff --git a/newsfragments/+0f77c990.bugfix.rst b/newsfragments/+0f77c990.bugfix.rst deleted file mode 100644 index 99c0dfca..00000000 --- a/newsfragments/+0f77c990.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -When inferring the caller in ``files()`` correctly detect one's own module even when the resources package source is not present. (python/cpython#123085) \ No newline at end of file From f1350e413775a9e79e20779cc9705e28a1c55900 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 07:05:32 -0400 Subject: [PATCH 432/496] 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 75b301c95ce3d75626c949a26e650deceba8e62c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:02:36 -0400 Subject: [PATCH 433/496] =?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 Ref jaraco/zipp#123 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..32d87a19 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,3 +12,7 @@ explicit_package_bases = True # Disable overload-overlap due to many false-positives disable_error_code = overload-overlap + +# jaraco/zipp#123 +[mypy-zipp] +ignore_missing_imports = True From 2fecb5e596c7b6ff2d3f98af0ac63c9f38401c5a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:05:35 -0400 Subject: [PATCH 434/496] =?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 Ref jaraco/jaraco.test#7 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 32d87a19..b136facd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,3 +16,7 @@ disable_error_code = overload-overlap # jaraco/zipp#123 [mypy-zipp] ignore_missing_imports = True + +# jaraco/jaraco.test#7 +[mypy-jaraco.test.*] +ignore_missing_imports = True From 045dde42b16581b99539667a87d61252bf4d08ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:06:25 -0400 Subject: [PATCH 435/496] =?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 --- importlib_resources/_common.py | 4 ++-- importlib_resources/compat/py39.py | 4 ++-- importlib_resources/simple.py | 2 +- importlib_resources/tests/test_files.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index e95371cd..70715111 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -66,10 +66,10 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: # zipimport.zipimporter does not support weak references, resulting in a # TypeError. That seems terrible. spec = package.__spec__ - reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr] if reader is None: return None - return reader(spec.name) # type: ignore + return reader(spec.name) # type: ignore[union-attr] @functools.singledispatch diff --git a/importlib_resources/compat/py39.py b/importlib_resources/compat/py39.py index ab87b9dc..ed5abd5e 100644 --- a/importlib_resources/compat/py39.py +++ b/importlib_resources/compat/py39.py @@ -5,6 +5,6 @@ if sys.version_info >= (3, 10): - from zipfile import Path as ZipPath # type: ignore + from zipfile import Path as ZipPath else: - from zipp import Path as ZipPath # type: ignore + from zipp import Path as ZipPath diff --git a/importlib_resources/simple.py b/importlib_resources/simple.py index 96f117fe..2e75299b 100644 --- a/importlib_resources/simple.py +++ b/importlib_resources/simple.py @@ -77,7 +77,7 @@ class ResourceHandle(Traversable): def __init__(self, parent: ResourceContainer, name: str): self.parent = parent - self.name = name # type: ignore + self.name = name # type: ignore[misc] def is_file(self): return True diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 9cb55d06..311581c9 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -86,7 +86,7 @@ def test_module_resources(self): """ A module can have resources found adjacent to the module. """ - import mod + import mod # type: ignore[import-not-found] actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') assert actual == self.spec['res.txt'] From f3ad28a14feb85dc2631a8e3658f090ed0b7522d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:14:58 -0400 Subject: [PATCH 436/496] =?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 Update to jaraco.path 3.7.1 with fixes for mypy nitpicks. --- importlib_resources/tests/_path.py | 50 ++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/importlib_resources/tests/_path.py b/importlib_resources/tests/_path.py index 1f97c961..b144628c 100644 --- a/importlib_resources/tests/_path.py +++ b/importlib_resources/tests/_path.py @@ -2,15 +2,44 @@ import functools from typing import Dict, Union +from typing import runtime_checkable +from typing import Protocol #### -# from jaraco.path 3.4.1 +# from jaraco.path 3.7.1 -FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore +class Symlink(str): + """ + A string indicating the target of a symlink. + """ + + +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] + + +@runtime_checkable +class TreeMaker(Protocol): + def __truediv__(self, *args, **kwargs): ... # pragma: no cover + + def mkdir(self, **kwargs): ... # pragma: no cover + + def write_text(self, content, **kwargs): ... # pragma: no cover + + def write_bytes(self, content): ... # pragma: no cover -def build(spec: FilesSpec, prefix=pathlib.Path()): + def symlink_to(self, target): ... # pragma: no cover + + +def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] + + +def build( + spec: FilesSpec, + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] +): """ Build a set of files/directories, as described by the spec. @@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()): ... "__init__.py": "", ... }, ... "baz.py": "# Some code", - ... } + ... "bar.py": Symlink("baz.py"), + ... }, + ... "bing": Symlink("foo"), ... } >>> target = getfixture('tmp_path') >>> build(spec, target) >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') '# Some code' + >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8') + '# Some code' """ for name, contents in spec.items(): - create(contents, pathlib.Path(prefix) / name) + create(contents, _ensure_tree_maker(prefix) / name) @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore + build(content, prefix=path) # type: ignore[arg-type] @create.register @@ -52,5 +85,10 @@ def _(content: str, path): path.write_text(content, encoding='utf-8') +@create.register +def _(content: Symlink, path): + path.symlink_to(content) + + # end from jaraco.path #### From 9689f8f82ca838cd58d3d0d80828785ada7798c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:21:44 -0400 Subject: [PATCH 437/496] Finalize --- NEWS.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 5f1ab11e..fff609c1 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,9 @@ +v6.4.4 +====== + +No significant changes. + + v6.4.3 ====== From 6a0b3af5640f4cbe8134440ff723979b470fa173 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2024 09:43:10 -0400 Subject: [PATCH 438/496] Add reference to development methodology. Ref python/cpython#123144 --- importlib_resources/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index ec4441c9..723c9f9e 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -1,4 +1,11 @@ -"""Read resources contained within a package.""" +""" +Read resources contained within a package. + +This codebase is shared between importlib.resources in the stdlib +and importlib_resources in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" from ._common import ( as_file, From 57b8aa81d77416805dcaaa22d5d45fef3e8b331c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 25 Aug 2024 09:29:10 +0100 Subject: [PATCH 439/496] 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 440/496] 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 441/496] 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 0c326f3f77b2420163f73d97f8fbd090fa49147d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 13:13:06 -0400 Subject: [PATCH 442/496] 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 443/496] 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 444/496] 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 445/496] 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 d84ca376316016420297fbc310ba181ca7d2864d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Sep 2024 08:22:16 -0400 Subject: [PATCH 446/496] Fix typo in _temp_path comment. Ref python/cpython#123597 --- importlib_resources/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 70715111..f065d493 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -183,7 +183,7 @@ def _(path): @contextlib.contextmanager def _temp_path(dir: tempfile.TemporaryDirectory): """ - Wrap tempfile.TemporyDirectory to return a pathlib object. + Wrap tempfile.TemporaryDirectory to return a pathlib object. """ with dir as result: yield pathlib.Path(result) From 4875bc5179938324d157d9917b3a0bfb5fca8dd1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2024 12:52:25 -0400 Subject: [PATCH 447/496] Add type annotations for _candidate_paths --- importlib_resources/readers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index abfcde73..dc649cfe 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import contextlib import itertools @@ -5,6 +7,7 @@ import operator import re import warnings +from collections.abc import Iterator from . import abc @@ -150,12 +153,12 @@ def _resolve(cls, path_str) -> abc.Traversable: return dir @classmethod - def _candidate_paths(cls, path_str): + def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]: yield pathlib.Path(path_str) yield from cls._resolve_zip_path(path_str) @staticmethod - def _resolve_zip_path(path_str): + def _resolve_zip_path(path_str: str): for match in reversed(list(re.finditer(r'[\\/]', path_str))): with contextlib.suppress( FileNotFoundError, From 47d73b1e7787cd66ee57be676f2385d2183f78ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 11:49:50 -0400 Subject: [PATCH 448/496] Add test capturing failure when resolving the MultiplexedPath for a namespace package with non-path elements in the path. Ref #311 --- importlib_resources/tests/test_files.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 311581c9..6fdcdef1 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -8,6 +8,8 @@ import importlib import contextlib +import pytest + import importlib_resources as resources from ..abc import Traversable from . import util @@ -60,6 +62,27 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): MODULE = 'namespacedata01' + @pytest.mark.xfail(reason="#311") + def test_non_paths_in_dunder_path(self): + """ + Non-path items in a namespace package's ``__path__`` are ignored. + + As reported in python/importlib_resources#311, some tools + like Setuptools, when creating editable packages, will inject + non-paths into a namespace package's ``__path__``, a + sentinel like + ``__editable__.sample_namespace-1.0.finder.__path_hook__`` + to cause the ``PathEntryFinder`` to be called when searching + for packages. In that case, resources should still be loadable. + """ + import namespacedata01 + + namespacedata01.__path__.append( + '__editable__.sample_namespace-1.0.finder.__path_hook__' + ) + + resources.files(namespacedata01) + class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): ZIP_MODULE = 'namespacedata01' From 2c145c5b1ff95290794b2cb63e5c924e1847456d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 12:11:53 -0400 Subject: [PATCH 449/496] Omit sentinel values from a namespace path. When editable installs create sentinels, as they are not a valid directory, they're unsuitable for constructing a `MultiplexedPath`. Filter them out. Fixes #311 --- importlib_resources/readers.py | 12 ++++++++---- importlib_resources/tests/test_files.py | 3 --- newsfragments/311.bugfix.rst | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 newsfragments/311.bugfix.rst diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index dc649cfe..4f761c64 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -138,19 +138,23 @@ class NamespaceReader(abc.TraversableResources): def __init__(self, namespace_path): if 'NamespacePath' not in str(namespace_path): raise ValueError('Invalid path') - self.path = MultiplexedPath(*map(self._resolve, namespace_path)) + self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path))) @classmethod - def _resolve(cls, path_str) -> abc.Traversable: + def _resolve(cls, path_str) -> abc.Traversable | None: r""" Given an item from a namespace path, resolve it to a Traversable. path_str might be a directory on the filesystem or a path to a zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or ``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. + + path_str might also be a sentinel used by editable packages to + trigger other behaviors (see python/importlib_resources#311). + In that case, return None. """ - (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) - return dir + dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) + return next(dirs, None) @classmethod def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]: diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 6fdcdef1..f1fe2337 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -8,8 +8,6 @@ import importlib import contextlib -import pytest - import importlib_resources as resources from ..abc import Traversable from . import util @@ -62,7 +60,6 @@ class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase): MODULE = 'namespacedata01' - @pytest.mark.xfail(reason="#311") def test_non_paths_in_dunder_path(self): """ Non-path items in a namespace package's ``__path__`` are ignored. diff --git a/newsfragments/311.bugfix.rst b/newsfragments/311.bugfix.rst new file mode 100644 index 00000000..56655f77 --- /dev/null +++ b/newsfragments/311.bugfix.rst @@ -0,0 +1 @@ +Omit sentinel values from a namespace path. \ No newline at end of file From 284148b005b57031a354402c446473f53cab2c49 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2024 13:00:41 -0400 Subject: [PATCH 450/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/311.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/311.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index fff609c1..36aac377 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.4.5 +====== + +Bugfixes +-------- + +- Omit sentinel values from a namespace path. (#311) + + v6.4.4 ====== diff --git a/newsfragments/311.bugfix.rst b/newsfragments/311.bugfix.rst deleted file mode 100644 index 56655f77..00000000 --- a/newsfragments/311.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Omit sentinel values from a namespace path. \ No newline at end of file From a675458e1a7d6ae81d0d441338a74dc98ffc5a61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Sep 2024 10:16:01 -0400 Subject: [PATCH 451/496] 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 452/496] 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 453/496] 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 8684c7a028b65381ec6c6724e2f9c9bea7df0aee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 22:27:33 -0400 Subject: [PATCH 454/496] Add workaround for broken importlib.resources when an editable sentinel is present. Closes #318 --- importlib_resources/future/adapters.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 0e9764ba..5cf17556 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -23,6 +23,13 @@ def wrapper(*args, **kwargs): except NotADirectoryError: # MultiplexedPath may fail on zip subdirectory return + except ValueError as exc: + # NamespaceReader in stdlib may fail for editable installs + # (python/importlib_resources#311, python/importlib_resources#318) + # Remove after bugfix applied to Python 3.13. + if "not enough values to unpack" not in str(exc): + raise + return # Python 3.10+ mod_name = reader.__class__.__module__ if mod_name.startswith('importlib.') and mod_name.endswith('readers'): From c00a2b75d0604db2e159838761a79d93b658f1e2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 12 Sep 2024 05:00:20 -0400 Subject: [PATCH 455/496] Separate bpo from Python issue numbers. --- docs/conf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a8abd1dc..570346b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,8 +27,12 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(python/cpython#|Python #|bpo-)(?P\d+)', - url='http://bugs.python.org/issue{python}', + pattern=r'(python/cpython#|Python #)(?P\d+)', + url='https://github.com/python/cpython/issues/{python}', + ), + dict( + pattern=r'bpo-(?P\d+)', + url='http://bugs.python.org/issue{bpo}', ), ], ), From 313bed20576c7ba6097ea00aa1c488602ee7e29d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 12 Sep 2024 10:14:46 -0400 Subject: [PATCH 456/496] gh-123994: Generate utf-16 file using little endian and BOM. (#123995) --- importlib_resources/tests/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index a4eafac3..5f755178 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -145,7 +145,7 @@ def test_useless_loader(self): data01={ '__init__.py': '', 'binary.file': bytes(range(4)), - 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-16.file': '\ufeffHello, UTF-16 world!\n'.encode('utf-16-le'), 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), 'subdirectory': { '__init__.py': '', @@ -160,7 +160,7 @@ def test_useless_loader(self): }, namespacedata01={ 'binary.file': bytes(range(4)), - 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-16.file': '\ufeffHello, UTF-16 world!\n'.encode('utf-16-le'), 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), 'subdirectory': { 'binary.file': bytes(range(12, 16)), From cc5d9cbb6e76cee1dc1c75401a1fa605de333016 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 18 Sep 2024 14:14:34 +0200 Subject: [PATCH 457/496] gh-123085: _compile_importlib: Avoid copying sources before compilation (GH-124131) Co-authored-by: Jason R. Coombs --- importlib_resources/tests/test_files.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index f1fe2337..bca626c0 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -154,18 +154,17 @@ def test_implicit_files_submodule(self): def _compile_importlib(self): """ Make a compiled-only copy of the importlib resources package. + + Currently only code is copied, as importlib resources doesn't itself + have any resources. """ bin_site = self.fixtures.enter_context(os_helper.temp_dir()) c_resources = pathlib.Path(bin_site, 'c_resources') sources = pathlib.Path(resources.__file__).parent - shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__']) - - for dirpath, _, filenames in os.walk(c_resources): - for filename in filenames: - source_path = pathlib.Path(dirpath) / filename - cfile = source_path.with_suffix('.pyc') - py_compile.compile(source_path, cfile) - pathlib.Path.unlink(source_path) + + for source_path in sources.glob('**/*.py'): + c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix('.pyc') + py_compile.compile(source_path, c_path) self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): From 9d4af05bac271932e3d6ab9307383da34b575e07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 18 Sep 2024 21:40:41 -0400 Subject: [PATCH 458/496] =?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 --- importlib_resources/tests/test_files.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index bca626c0..f45a229c 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -163,7 +163,9 @@ def _compile_importlib(self): sources = pathlib.Path(resources.__file__).parent for source_path in sources.glob('**/*.py'): - c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix('.pyc') + c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix( + '.pyc' + ) py_compile.compile(source_path, c_path) self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) From 62b6678a32087ed3bfc8ff19761764340295834e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 26 Oct 2024 00:12:59 +0100 Subject: [PATCH 459/496] 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 460/496] 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 461/496] 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 462/496] 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 463/496] 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 4ed8bffcbc354ccfc67031b86d0848cb61712c11 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 23 Dec 2024 16:19:37 -0500 Subject: [PATCH 464/496] =?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 --- importlib_resources/tests/test_functional.py | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 1851edfb..ffe12db4 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -182,17 +182,23 @@ def test_contents(self): set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with self.assertRaises(OSError), warnings_helper.check_warnings(( - ".*contents.*", - DeprecationWarning, - )): + with ( + self.assertRaises(OSError), + warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )), + ): list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): - with self.assertRaises(OSError), warnings_helper.check_warnings(( - ".*contents.*", - DeprecationWarning, - )): + with ( + self.assertRaises(OSError), + warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )), + ): list(resources.contents(self.anchor01, *path_parts)) with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') From ef8977791bf94b18aa71922d1d58f3ffc8acd161 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 09:53:54 -0500 Subject: [PATCH 465/496] Remove unused imports. --- importlib_resources/tests/test_files.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index f45a229c..b762d4db 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,7 +1,5 @@ -import os import pathlib import py_compile -import shutil import textwrap import unittest import warnings From 39a607d25def76ef760334a494554847da8c8f0f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:13 -0500 Subject: [PATCH 466/496] 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 2a95f57deb157f48d81e3e6b38345e5615e744ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:36 -0500 Subject: [PATCH 467/496] Re-enable type checks. --- importlib_resources/tests/test_files.py | 2 +- pyproject.toml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index b762d4db..8d87a01e 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -70,7 +70,7 @@ def test_non_paths_in_dunder_path(self): to cause the ``PathEntryFinder`` to be called when searching for packages. In that case, resources should still be loadable. """ - import namespacedata01 + import namespacedata01 # type: ignore[import-not-found] namespacedata01.__path__.append( '__editable__.sample_namespace-1.0.finder.__path_hook__' diff --git a/pyproject.toml b/pyproject.toml index d08547f7..608e7515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 From 2917dc4a76219e3d0b13a9e5f85fd7079062c824 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:32:51 -0500 Subject: [PATCH 468/496] Apply import sort fixers from ruff. --- importlib_resources/__init__.py | 7 ++----- importlib_resources/_common.py | 12 ++++++------ importlib_resources/_functional.py | 3 +-- importlib_resources/abc.py | 14 +++++++++++--- importlib_resources/compat/py38.py | 2 -- importlib_resources/compat/py39.py | 1 - importlib_resources/future/adapters.py | 2 +- importlib_resources/readers.py | 3 +-- importlib_resources/tests/_path.py | 8 ++------ .../tests/test_compatibilty_files.py | 1 - importlib_resources/tests/test_contents.py | 1 + importlib_resources/tests/test_custom.py | 5 +++-- importlib_resources/tests/test_files.py | 7 ++++--- importlib_resources/tests/test_functional.py | 7 +++---- importlib_resources/tests/test_open.py | 1 + importlib_resources/tests/test_path.py | 1 + importlib_resources/tests/test_read.py | 3 ++- importlib_resources/tests/test_reader.py | 2 +- importlib_resources/tests/test_resource.py | 3 ++- importlib_resources/tests/util.py | 12 +++++------- 20 files changed, 47 insertions(+), 48 deletions(-) diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 723c9f9e..27d6c7f8 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -8,12 +8,11 @@ """ from ._common import ( + Anchor, + Package, as_file, files, - Package, - Anchor, ) - from ._functional import ( contents, is_resource, @@ -23,10 +22,8 @@ read_binary, read_text, ) - from .abc import ResourceReader - __all__ = [ 'Package', 'Anchor', diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index f065d493..5f41c265 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -1,15 +1,15 @@ +import contextlib +import functools +import importlib +import inspect +import itertools import os import pathlib import tempfile -import functools -import contextlib import types -import importlib -import inspect import warnings -import itertools +from typing import Optional, Union, cast -from typing import Union, Optional, cast from .abc import ResourceReader, Traversable Package = Union[types.ModuleType, str] diff --git a/importlib_resources/_functional.py b/importlib_resources/_functional.py index f59416f2..5a3ca0a2 100644 --- a/importlib_resources/_functional.py +++ b/importlib_resources/_functional.py @@ -2,8 +2,7 @@ import warnings -from ._common import files, as_file - +from ._common import as_file, files _MISSING = object() diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 7a58dd2f..c0a4cb95 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -2,12 +2,20 @@ import io import itertools import pathlib -from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional -from typing import runtime_checkable, Protocol +from typing import ( + Any, + BinaryIO, + Iterable, + Iterator, + NoReturn, + Optional, + Protocol, + Text, + runtime_checkable, +) from .compat.py38 import StrPath - __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/importlib_resources/compat/py38.py b/importlib_resources/compat/py38.py index 4d548257..80940e94 100644 --- a/importlib_resources/compat/py38.py +++ b/importlib_resources/compat/py38.py @@ -1,9 +1,7 @@ import os import sys - from typing import Union - if sys.version_info >= (3, 9): StrPath = Union[str, os.PathLike[str]] else: diff --git a/importlib_resources/compat/py39.py b/importlib_resources/compat/py39.py index ed5abd5e..684d3c63 100644 --- a/importlib_resources/compat/py39.py +++ b/importlib_resources/compat/py39.py @@ -1,6 +1,5 @@ import sys - __all__ = ['ZipPath'] diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 5cf17556..239e52b7 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -3,7 +3,7 @@ from contextlib import suppress from types import SimpleNamespace -from .. import readers, _adapters +from .. import _adapters, readers def _block_standard(reader_getter): diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 4f761c64..99884b6a 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -3,14 +3,13 @@ import collections import contextlib import itertools -import pathlib import operator +import pathlib import re import warnings from collections.abc import Iterator from . import abc - from ._itertools import only from .compat.py39 import ZipPath diff --git a/importlib_resources/tests/_path.py b/importlib_resources/tests/_path.py index b144628c..0033983d 100644 --- a/importlib_resources/tests/_path.py +++ b/importlib_resources/tests/_path.py @@ -1,10 +1,6 @@ -import pathlib import functools - -from typing import Dict, Union -from typing import runtime_checkable -from typing import Protocol - +import pathlib +from typing import Dict, Protocol, Union, runtime_checkable #### # from jaraco.path 3.7.1 diff --git a/importlib_resources/tests/test_compatibilty_files.py b/importlib_resources/tests/test_compatibilty_files.py index 13ad0dfb..e8aac284 100644 --- a/importlib_resources/tests/test_compatibilty_files.py +++ b/importlib_resources/tests/test_compatibilty_files.py @@ -2,7 +2,6 @@ import unittest import importlib_resources as resources - from importlib_resources._adapters import ( CompatibilityFiles, wrap_spec, diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 741a7407..dcb872ec 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -1,4 +1,5 @@ import unittest + import importlib_resources as resources from . import util diff --git a/importlib_resources/tests/test_custom.py b/importlib_resources/tests/test_custom.py index 86c65676..25ae0e75 100644 --- a/importlib_resources/tests/test_custom.py +++ b/importlib_resources/tests/test_custom.py @@ -1,10 +1,11 @@ -import unittest import contextlib import pathlib +import unittest import importlib_resources as resources + from .. import abc -from ..abc import TraversableResources, ResourceReader +from ..abc import ResourceReader, TraversableResources from . import util from .compat.py39 import os_helper diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 8d87a01e..be206603 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,15 +1,16 @@ +import contextlib +import importlib import pathlib import py_compile import textwrap import unittest import warnings -import importlib -import contextlib import importlib_resources as resources + from ..abc import Traversable from . import util -from .compat.py39 import os_helper, import_helper +from .compat.py39 import import_helper, os_helper @contextlib.contextmanager diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index ffe12db4..2285389e 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -1,12 +1,11 @@ -import unittest -import os import importlib - -from .compat.py39 import warnings_helper +import os +import unittest import importlib_resources as resources from . import util +from .compat.py39 import warnings_helper # Since the functional API forwards to Traversable, we only test # filesystem resources here -- not zip files, namespace packages etc. diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index c40bb8c6..8a4b68e3 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -1,6 +1,7 @@ import unittest import importlib_resources as resources + from . import util diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index 1e30f2bc..0be673d2 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -3,6 +3,7 @@ import unittest import importlib_resources as resources + from . import util diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 6780a2d1..216c8feb 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -1,8 +1,9 @@ import unittest +from importlib import import_module + import importlib_resources as resources from . import util -from importlib import import_module class CommonBinaryTests(util.CommonTests, unittest.TestCase): diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 0a77eb40..f8cfd8de 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -1,8 +1,8 @@ import os.path import pathlib import unittest - from importlib import import_module + from importlib_resources.readers import MultiplexedPath, NamespaceReader from . import util diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index a0da6a35..c80afdc7 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -1,8 +1,9 @@ import unittest +from importlib import import_module + import importlib_resources as resources from . import util -from importlib import import_module class ResourceTests: diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 5f755178..07d1529a 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -1,18 +1,16 @@ import abc +import contextlib import importlib import io +import pathlib import sys import types -import pathlib -import contextlib +from importlib.machinery import ModuleSpec from ..abc import ResourceReader -from .compat.py39 import import_helper, os_helper -from . import zip as zip_ from . import _path - - -from importlib.machinery import ModuleSpec +from . import zip as zip_ +from .compat.py39 import import_helper, os_helper class Reader(ResourceReader): From 8e9503d90943335369219ba3bc088d2bcd4bca7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 09:54:39 -0500 Subject: [PATCH 469/496] Add type annotations for Traversable.open. Closes #317. --- importlib_resources/abc.py | 18 +++++++++++++++--- newsfragments/317.feature.rst | 1 + 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 newsfragments/317.feature.rst diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index c0a4cb95..39b0d375 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,5 +1,4 @@ import abc -import io import itertools import pathlib from typing import ( @@ -11,9 +10,14 @@ Optional, Protocol, Text, + TextIO, + Union, + overload, runtime_checkable, ) +from typing_extensions import Literal + from .compat.py38 import StrPath __all__ = ["ResourceReader", "Traversable", "TraversableResources"] @@ -138,8 +142,16 @@ def __truediv__(self, child: StrPath) -> "Traversable": """ return self.joinpath(child) + @overload + def open(self, mode: Literal['r'] = 'r', *args: Any, **kwargs: Any) -> TextIO: ... + + @overload + def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ... + @abc.abstractmethod - def open(self, mode='r', *args, **kwargs): + def open( + self, mode: str = 'r', *args: Any, **kwargs: Any + ) -> Union[TextIO, BinaryIO]: """ mode may be 'r' or 'rb' to open as text or binary. Return a handle suitable for reading (same as pathlib.Path.open). @@ -166,7 +178,7 @@ class TraversableResources(ResourceReader): def files(self) -> "Traversable": """Return a Traversable object for the loaded package.""" - def open_resource(self, resource: StrPath) -> io.BufferedReader: + def open_resource(self, resource: StrPath) -> BinaryIO: return self.files().joinpath(resource).open('rb') def resource_path(self, resource: Any) -> NoReturn: diff --git a/newsfragments/317.feature.rst b/newsfragments/317.feature.rst new file mode 100644 index 00000000..25b1a97b --- /dev/null +++ b/newsfragments/317.feature.rst @@ -0,0 +1 @@ +Add type annotations for Traversable.open. From 355a10db1644edcd92f876bd9aec4dd3839f7e75 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:37:31 -0500 Subject: [PATCH 470/496] Remove Python 3.8 compatibility code. --- importlib_resources/abc.py | 3 ++- importlib_resources/compat/py38.py | 9 --------- newsfragments/+3f742cf7.feature.rst | 1 + 3 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 importlib_resources/compat/py38.py create mode 100644 newsfragments/+3f742cf7.feature.rst diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 39b0d375..84631c7b 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,5 +1,6 @@ import abc import itertools +import os import pathlib from typing import ( Any, @@ -18,7 +19,7 @@ from typing_extensions import Literal -from .compat.py38 import StrPath +StrPath = Union[str, os.PathLike[str]] __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/importlib_resources/compat/py38.py b/importlib_resources/compat/py38.py deleted file mode 100644 index 80940e94..00000000 --- a/importlib_resources/compat/py38.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -import sys -from typing import Union - -if sys.version_info >= (3, 9): - StrPath = Union[str, os.PathLike[str]] -else: - # PathLike is only subscriptable at runtime in 3.9+ - StrPath = Union[str, "os.PathLike[str]"] diff --git a/newsfragments/+3f742cf7.feature.rst b/newsfragments/+3f742cf7.feature.rst new file mode 100644 index 00000000..f171cdde --- /dev/null +++ b/newsfragments/+3f742cf7.feature.rst @@ -0,0 +1 @@ +Require Python 3.9 or later. From fa27acb836d98ffa87794de37d33b7188aad082f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:38:40 -0500 Subject: [PATCH 471/496] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+3f742cf7.feature.rst | 1 - newsfragments/317.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+3f742cf7.feature.rst delete mode 100644 newsfragments/317.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 36aac377..c3612c17 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v6.5.0 +====== + +Features +-------- + +- Add type annotations for Traversable.open. (#317) +- Require Python 3.9 or later. + + v6.4.5 ====== diff --git a/newsfragments/+3f742cf7.feature.rst b/newsfragments/+3f742cf7.feature.rst deleted file mode 100644 index f171cdde..00000000 --- a/newsfragments/+3f742cf7.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.9 or later. diff --git a/newsfragments/317.feature.rst b/newsfragments/317.feature.rst deleted file mode 100644 index 25b1a97b..00000000 --- a/newsfragments/317.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add type annotations for Traversable.open. From 912a9e5dc1bc15352d053fa55c02c8a90eb7b3c6 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 25 Nov 2024 11:13:49 -0600 Subject: [PATCH 472/496] Demonstrate python/cpython#127012 This adds an in-memory finder, loader, and traversable implementation, which allows the `Traversable` protocol and concrete methods to be tested. This additional infrastructure demonstrates python/cpython#127012, but also highlights that the `Traversable.joinpath()` concrete method raises `TraversalError` which is not getting caught in several places. --- importlib_resources/tests/test_functional.py | 21 ++-- importlib_resources/tests/test_util.py | 35 +++++++ importlib_resources/tests/util.py | 105 ++++++++++++++++++- 3 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 importlib_resources/tests/test_util.py diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 2285389e..d65a52c1 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -7,10 +7,6 @@ from . import util from .compat.py39 import warnings_helper -# Since the functional API forwards to Traversable, we only test -# filesystem resources here -- not zip files, namespace packages etc. -# We do test for two kinds of Anchor, though. - class StringAnchorMixin: anchor01 = 'data01' @@ -27,7 +23,7 @@ def anchor02(self): return importlib.import_module('data02') -class FunctionalAPIBase(util.DiskSetup): +class FunctionalAPIBase: def setUp(self): super().setUp() self.load_fixture('data02') @@ -244,17 +240,28 @@ def test_text_errors(self): ) -class FunctionalAPITest_StringAnchor( +class FunctionalAPITest_StringAnchor_Disk( StringAnchorMixin, FunctionalAPIBase, + util.DiskSetup, unittest.TestCase, ): pass -class FunctionalAPITest_ModuleAnchor( +class FunctionalAPITest_ModuleAnchor_Disk( ModuleAnchorMixin, FunctionalAPIBase, + util.DiskSetup, + unittest.TestCase, +): + pass + + +class FunctionalAPITest_StringAnchor_Memory( + StringAnchorMixin, + FunctionalAPIBase, + util.MemorySetup, unittest.TestCase, ): pass diff --git a/importlib_resources/tests/test_util.py b/importlib_resources/tests/test_util.py new file mode 100644 index 00000000..ba3ba28c --- /dev/null +++ b/importlib_resources/tests/test_util.py @@ -0,0 +1,35 @@ +import unittest + +from .util import MemorySetup, Traversable + + +class TestMemoryTraversableImplementation(unittest.TestCase): + def test_concrete_methods_are_not_overridden(self): + """`MemoryTraversable` must not override `Traversable` concrete methods. + + This test is not an attempt to enforce a particular `Traversable` protocol; + it merely catches changes in the `Traversable` abstract/concrete methods + that have not been mirrored in the `MemoryTraversable` subclass. + """ + + traversable_concrete_methods = { + method + for method, value in Traversable.__dict__.items() + if callable(value) and method not in Traversable.__abstractmethods__ + } + memory_traversable_concrete_methods = { + method + for method, value in MemorySetup.MemoryTraversable.__dict__.items() + if callable(value) and not method.startswith("__") + } + overridden_methods = ( + memory_traversable_concrete_methods & traversable_concrete_methods + ) + + if overridden_methods: + raise AssertionError( + "MemorySetup.MemoryTraversable overrides Traversable concrete methods, " + "which may mask problems in the Traversable protocol. " + "Please remove the following methods in MemoryTraversable: " + + ", ".join(overridden_methods) + ) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 07d1529a..2f709d3a 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -1,5 +1,6 @@ import abc import contextlib +import functools import importlib import io import pathlib @@ -7,7 +8,7 @@ import types from importlib.machinery import ModuleSpec -from ..abc import ResourceReader +from ..abc import ResourceReader, Traversable, TraversableResources from . import _path from . import zip as zip_ from .compat.py39 import import_helper, os_helper @@ -200,5 +201,107 @@ def tree_on_path(self, spec): self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir)) +class MemorySetup(ModuleSetup): + """Support loading a module in memory.""" + + MODULE = 'data01' + + def load_fixture(self, module): + self.fixtures.enter_context(self.augment_sys_metapath(module)) + return importlib.import_module(module) + + @contextlib.contextmanager + def augment_sys_metapath(self, module): + finder_instance = self.MemoryFinder(module) + sys.meta_path.append(finder_instance) + yield + sys.meta_path.remove(finder_instance) + + class MemoryFinder(importlib.abc.MetaPathFinder): + def __init__(self, module): + self._module = module + + def find_spec(self, fullname, path, target=None): + if fullname != self._module: + return None + + return importlib.machinery.ModuleSpec( + name=fullname, + loader=MemorySetup.MemoryLoader(self._module), + is_package=True, + ) + + class MemoryLoader(importlib.abc.Loader): + def __init__(self, module): + self._module = module + + def exec_module(self, module): + pass + + def get_resource_reader(self, fullname): + return MemorySetup.MemoryTraversableResources(self._module, fullname) + + class MemoryTraversableResources(TraversableResources): + def __init__(self, module, fullname): + self._module = module + self._fullname = fullname + + def files(self): + return MemorySetup.MemoryTraversable(self._module, self._fullname) + + class MemoryTraversable(Traversable): + """Implement only the abstract methods of `Traversable`. + + Besides `.__init__()`, no other methods may be implemented or overridden. + This is critical for validating the concrete `Traversable` implementations. + """ + + def __init__(self, module, fullname): + self._module = module + self._fullname = fullname + + def iterdir(self): + path = pathlib.PurePosixPath(self._fullname) + directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + if not isinstance(directory, dict): + # Filesystem openers raise OSError, and that exception is mirrored here. + raise OSError(f"{self._fullname} is not a directory") + for path in directory: + yield MemorySetup.MemoryTraversable( + self._module, f"{self._fullname}/{path}" + ) + + def is_dir(self) -> bool: + path = pathlib.PurePosixPath(self._fullname) + # Fully traverse the `fixtures` dictionary. + # This should be wrapped in a `try/except KeyError` + # but it is not currently needed, and lowers the code coverage numbers. + directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + return isinstance(directory, dict) + + def is_file(self) -> bool: + path = pathlib.PurePosixPath(self._fullname) + directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + return not isinstance(directory, dict) + + def open(self, mode='r', encoding=None, errors=None, *_, **__): + path = pathlib.PurePosixPath(self._fullname) + contents = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + if isinstance(contents, dict): + # Filesystem openers raise OSError when attempting to open a directory, + # and that exception is mirrored here. + raise OSError(f"{self._fullname} is a directory") + if isinstance(contents, str): + contents = contents.encode("utf-8") + result = io.BytesIO(contents) + if "b" in mode: + return result + return io.TextIOWrapper(result, encoding=encoding, errors=errors) + + @property + def name(self): + return pathlib.PurePosixPath(self._fullname).name + + class CommonTests(DiskSetup, CommonTestsBase): pass From f10a2e9b2608cdf5006608dd7f2b29d3f0b1cbd2 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 25 Nov 2024 11:22:52 -0600 Subject: [PATCH 473/496] Catch `TraversalError`, raised by `Traversable.joinpath()` Exercising the `Traversable` protocol's concrete methods has highlighted that `.joinpath()` raises `TraversalError`, which needs to be caught in several places. This is primarily resolved within the test suite, but implicates the `is_resource()` function as well. --- importlib_resources/_functional.py | 6 +++++- importlib_resources/tests/test_functional.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/importlib_resources/_functional.py b/importlib_resources/_functional.py index 5a3ca0a2..b08a5c6e 100644 --- a/importlib_resources/_functional.py +++ b/importlib_resources/_functional.py @@ -3,6 +3,7 @@ import warnings from ._common import as_file, files +from .abc import TraversalError _MISSING = object() @@ -41,7 +42,10 @@ def is_resource(anchor, *path_names): Otherwise returns ``False``. """ - return _get_resource(anchor, path_names).is_file() + try: + return _get_resource(anchor, path_names).is_file() + except TraversalError: + return False def contents(anchor, *path_names): diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index d65a52c1..9eb2d815 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -72,7 +72,7 @@ def test_read_text(self): # fail with PermissionError rather than IsADirectoryError with self.assertRaises(OSError): resources.read_text(self.anchor01) - with self.assertRaises(OSError): + with self.assertRaises((OSError, resources.abc.TraversalError)): resources.read_text(self.anchor01, 'no-such-file') with self.assertRaises(UnicodeDecodeError): resources.read_text(self.anchor01, 'utf-16.file') @@ -120,7 +120,7 @@ def test_open_text(self): # fail with PermissionError rather than IsADirectoryError with self.assertRaises(OSError): resources.open_text(self.anchor01) - with self.assertRaises(OSError): + with self.assertRaises((OSError, resources.abc.TraversalError)): resources.open_text(self.anchor01, 'no-such-file') with resources.open_text(self.anchor01, 'utf-16.file') as f: with self.assertRaises(UnicodeDecodeError): @@ -188,7 +188,7 @@ def test_contents(self): for path_parts in self._gen_resourcetxt_path_parts(): with ( - self.assertRaises(OSError), + self.assertRaises((OSError, resources.abc.TraversalError)), warnings_helper.check_warnings(( ".*contents.*", DeprecationWarning, From d001110ce83b32ad5fa7bae3932a8db1faccbb8b Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 25 Nov 2024 11:29:05 -0600 Subject: [PATCH 474/496] Resolve a `TypeError` lurking in the `read_text()` functional API `importlib_resources.read_text()` calls the `Traversable.read_text()` concrete method with an `errors` argument that doesn't exist in the method signature, resulting in an `TypeError`. This is resolved by adding an `errors` parameter to `Traversable.read_text()`. Fixes python/cpython#127012 --- importlib_resources/abc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 84631c7b..befe7991 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -93,11 +93,13 @@ def read_bytes(self) -> bytes: with self.open('rb') as strm: return strm.read() - def read_text(self, encoding: Optional[str] = None) -> str: + def read_text( + self, encoding: Optional[str] = None, errors: Optional[str] = None + ) -> str: """ Read contents of self as text """ - with self.open(encoding=encoding) as strm: + with self.open(encoding=encoding, errors=errors) as strm: return strm.read() @abc.abstractmethod From 72d550dfabc4a56fa22ec1e347b2672d546a39a4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 11:33:06 -0500 Subject: [PATCH 475/496] Consolidate MemoryTraversable._resolve. --- importlib_resources/tests/util.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 2f709d3a..0340c150 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -260,9 +260,18 @@ def __init__(self, module, fullname): self._module = module self._fullname = fullname - def iterdir(self): + def _resolve(self): + """ + Fully traverse the `fixtures` dictionary. + + This should be wrapped in a `try/except KeyError` + but it is not currently needed and lowers the code coverage numbers. + """ path = pathlib.PurePosixPath(self._fullname) - directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + return functools.reduce(lambda d, p: d[p], path.parts, fixtures) + + def iterdir(self): + directory = self._resolve() if not isinstance(directory, dict): # Filesystem openers raise OSError, and that exception is mirrored here. raise OSError(f"{self._fullname} is not a directory") @@ -272,21 +281,13 @@ def iterdir(self): ) def is_dir(self) -> bool: - path = pathlib.PurePosixPath(self._fullname) - # Fully traverse the `fixtures` dictionary. - # This should be wrapped in a `try/except KeyError` - # but it is not currently needed, and lowers the code coverage numbers. - directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) - return isinstance(directory, dict) + return isinstance(self._resolve(), dict) def is_file(self) -> bool: - path = pathlib.PurePosixPath(self._fullname) - directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures) - return not isinstance(directory, dict) + return not self.is_dir() def open(self, mode='r', encoding=None, errors=None, *_, **__): - path = pathlib.PurePosixPath(self._fullname) - contents = functools.reduce(lambda d, p: d[p], path.parts, fixtures) + contents = self._resolve() if isinstance(contents, dict): # Filesystem openers raise OSError when attempting to open a directory, # and that exception is mirrored here. From cf269ce50f496671f3b7fbc5e6292946ecc70e7d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 11:42:13 -0500 Subject: [PATCH 476/496] Replace unreachable block with simple assertion. Fixes diffcov failure. --- importlib_resources/tests/test_util.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/importlib_resources/tests/test_util.py b/importlib_resources/tests/test_util.py index ba3ba28c..de304b6f 100644 --- a/importlib_resources/tests/test_util.py +++ b/importlib_resources/tests/test_util.py @@ -26,10 +26,4 @@ def test_concrete_methods_are_not_overridden(self): memory_traversable_concrete_methods & traversable_concrete_methods ) - if overridden_methods: - raise AssertionError( - "MemorySetup.MemoryTraversable overrides Traversable concrete methods, " - "which may mask problems in the Traversable protocol. " - "Please remove the following methods in MemoryTraversable: " - + ", ".join(overridden_methods) - ) + assert not overridden_methods From 9a872e5dbceff32260e8ff19d039236304ee150c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 11:46:18 -0500 Subject: [PATCH 477/496] Add news fragment. --- newsfragments/321.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/321.bugfix.rst diff --git a/newsfragments/321.bugfix.rst b/newsfragments/321.bugfix.rst new file mode 100644 index 00000000..788cd326 --- /dev/null +++ b/newsfragments/321.bugfix.rst @@ -0,0 +1 @@ +Updated ``Traversable.read_text()`` to reflect the ``errors`` parameter (python/cpython#127012). From 78c4bda73c5d671cbbcfdf1430b6f2da03aeb04f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 11:54:59 -0500 Subject: [PATCH 478/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/321.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/321.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index c3612c17..48899f0e 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.5.1 +====== + +Bugfixes +-------- + +- Updated ``Traversable.read_text()`` to reflect the ``errors`` parameter (python/cpython#127012). (#321) + + v6.5.0 ====== diff --git a/newsfragments/321.bugfix.rst b/newsfragments/321.bugfix.rst deleted file mode 100644 index 788cd326..00000000 --- a/newsfragments/321.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Updated ``Traversable.read_text()`` to reflect the ``errors`` parameter (python/cpython#127012). From a61a948b6293ed46972909652909e2540242f197 Mon Sep 17 00:00:00 2001 From: "Peter St. John" Date: Fri, 3 Jan 2025 10:14:43 -0700 Subject: [PATCH 479/496] Add typing-extensions as a dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 608e7515..1e151587 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "zipp >= 3.1.0; python_version < '3.10'", + "zipp >= 3.1.0; typing-extensions >= 3.10; python_version < '3.10'", ] dynamic = ["version"] From 883380a5fcc482b67014b90b9aa7250d868c795a Mon Sep 17 00:00:00 2001 From: "Peter St. John" Date: Fri, 3 Jan 2025 11:45:51 -0700 Subject: [PATCH 480/496] Update pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1e151587..25487d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,8 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "zipp >= 3.1.0; typing-extensions >= 3.10; python_version < '3.10'", + "zipp >= 3.1.0; python_version < '3.10'", + "typing-extensions >= 3.10", ] dynamic = ["version"] From 6c1bc3cdb8f8a7d29252becc201508712040facc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 13:45:20 -0500 Subject: [PATCH 481/496] Rely on Literal from stdlib. --- importlib_resources/abc.py | 3 +-- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index befe7991..883d3328 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -8,6 +8,7 @@ Iterable, Iterator, NoReturn, + Literal, Optional, Protocol, Text, @@ -17,8 +18,6 @@ runtime_checkable, ) -from typing_extensions import Literal - StrPath = Union[str, os.PathLike[str]] __all__ = ["ResourceReader", "Traversable", "TraversableResources"] diff --git a/pyproject.toml b/pyproject.toml index 25487d56..608e7515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "zipp >= 3.1.0; python_version < '3.10'", - "typing-extensions >= 3.10", ] dynamic = ["version"] From 6569354ad2cf38b202fdeaf11b49e639b1a00fbc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 13:46:39 -0500 Subject: [PATCH 482/496] Add news fragment. --- newsfragments/323.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/323.bugfix.rst diff --git a/newsfragments/323.bugfix.rst b/newsfragments/323.bugfix.rst new file mode 100644 index 00000000..964c1194 --- /dev/null +++ b/newsfragments/323.bugfix.rst @@ -0,0 +1 @@ +Replaced reference to typing_extensions with stdlib Literal. From 7d3b2bfa1d8c53ecb64246ba16d8c632f8fcb6bf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 13:48:58 -0500 Subject: [PATCH 483/496] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/323.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/323.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 48899f0e..a7c95e80 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.5.2 +====== + +Bugfixes +-------- + +- Replaced reference to typing_extensions with stdlib Literal. (#323) + + v6.5.1 ====== diff --git a/newsfragments/323.bugfix.rst b/newsfragments/323.bugfix.rst deleted file mode 100644 index 964c1194..00000000 --- a/newsfragments/323.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Replaced reference to typing_extensions with stdlib Literal. From aee344d781920bba42ddbee4b4b44af29d7bab6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Feb 2025 10:44:24 -0500 Subject: [PATCH 484/496] 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 485/496] 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 486/496] 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 487/496] 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 488/496] 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 489/496] 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 490/496] 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 491/496] 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 492/496] 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 493/496] 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 494/496] 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 495/496] 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 496/496] 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"]