diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 45823064..00000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -omit = .tox/* - -[report] -show_missing = True diff --git a/.flake8 b/.flake8 deleted file mode 100644 index bc470da1..00000000 --- a/.flake8 +++ /dev/null @@ -1,13 +0,0 @@ -[flake8] -max-line-length = 88 - -# jaraco/skeleton#34 -max-complexity = 10 - -extend-ignore = - # Black creates whitespace before colon - E203 -exclude = - # Exclude the entire top-level __init__.py file since its only purpose is - # to expose the version string and to handle Python 2/3 compatibility. - importlib_resources/__init__.py diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e6805180..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -*.file binary -*.zip binary 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 }}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 2d00d652..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: tests - -on: [push, pull_request] - -jobs: - test: - strategy: - matrix: - python: [3.6, 3.8, 3.9] - platform: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - name: Install tox - run: | - python -m pip install tox - - name: Run tests - run: tox - - diffcov: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install tox - run: | - python -m pip install tox - - name: Evaluate coverage - run: tox - env: - TOXENV: diffcov - - release: - needs: test - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install tox - run: | - python -m pip install tox - - name: Release - run: tox -e release - env: - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 87274500..00000000 --- a/.gitignore +++ /dev/null @@ -1,102 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -/diffcov.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c15ab0c9..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -repos: -- repo: https://github.com/psf/black - rev: 20.8b1 - hooks: - - id: black - -- repo: https://github.com/asottile/blacken-docs - rev: v1.9.1 - hooks: - - id: blacken-docs diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index cc698548..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -python: - install: - - path: . - extra_requirements: - - docs diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index ced83de8..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,271 +0,0 @@ -v5.0.7 -====== - -* bpo-45419: Correct ``DegenerateFiles.Path`` ``.name`` - and ``.open()`` interfaces to match ``Traversable``. - -v5.0.6 -====== - -* bpo-38693: Prefer f-strings to ``.format`` calls. - -v5.0.5 -====== - -* #216: Make MultiplexedPath.name a property per the - spec. - -v5.0.4 -====== - -* Fixed non-hermetic test in test_reader, revealed by - GH-24670. - -v5.0.3 -====== - -* Simplified DegenerateFiles.Path. - -v5.0.2 -====== - -* #214: Added ``_adapters`` module to ensure that degenerate - ``files`` behavior can be made available for legacy loaders - whose resource readers don't implement it. Fixes issue where - backport compatibility module was masking this fallback - behavior only to discover the defect when applying changes to - CPython. - -v5.0.1 -====== - -* Remove pyinstaller hook for hidden 'trees' module. - -v5.0.0 -====== - -* Removed ``importlib_resources.trees``, deprecated since 1.3.0. - -v4.1.1 -====== - -* Fixed badges in README. - -v4.1.0 -====== - -* #209: Adopt - `jaraco/skeleton `_. - -* Cleaned up some straggling Python 2 compatibility code. - -* Refreshed test zip files without .pyc and .pyo files. - -v4.0.0 -====== - -* #108: Drop support for Python 2.7. Now requires Python 3.6+. - -v3.3.1 -====== - -* Minor cleanup. - -v3.3.0 -====== - -* #107: Drop support for Python 3.5. Now requires Python 2.7 or 3.6+. - -v3.2.1 -====== - -* #200: Minor fixes and improved tests for namespace package support. - -v3.2.0 -====== - -* #68: Resources in PEP 420 Namespace packages are now supported. - -v3.1.1 -====== - -* bpo-41490: ``contents`` is now also more aggressive about - consuming any iterator from the ``Reader``. - -v3.1.0 -====== - -* #110 and bpo-41490: ``path`` method is more aggressive about - releasing handles to zipfile objects early, enabling use-cases - like ``certifi`` to leave the context open but delete the underlying - zip file. - -v3.0.0 -====== - -* Package no longer exposes ``importlib_resources.__version__``. - Users that wish to inspect the version of ``importlib_resources`` - should instead invoke ``.version('importlib_resources')`` from - ``importlib-metadata`` ( - `stdlib `_ - or `backport `_) - directly. This change eliminates the dependency on - ``importlib_metadata``. Closes #100. -* Package now always includes its data. Closes #93. -* Declare hidden imports for PyInstaller. Closes #101. - -v2.0.1 -====== - -* Select pathlib and contextlib imports based on Python version - and avoid pulling in deprecated - [pathlib](https://pypi.org/project/pathlib). Closes #97. - -v2.0.0 -====== - -* Loaders are no longer expected to implement the - ``abc.TraversableResources`` interface, but are instead - expected to return ``TraversableResources`` from their - ``get_resource_reader`` method. - -v1.5.0 -====== - -* Traversable is now a Protocol instead of an Abstract Base - Class (Python 2.7 and Python 3.8+). - -* Traversable objects now require a ``.name`` property. - -v1.4.0 -====== - -* #79: Temporary files created will now reflect the filename of - their origin. - -v1.3.1 -====== - -* For improved compatibility, ``importlib_resources.trees`` is - now imported implicitly. Closes #88. - -v1.3.0 -====== - -* Add extensibility support for non-standard loaders to supply - ``Traversable`` resources. Introduces a new abstract base - class ``abc.TraversableResources`` that supersedes (but - implements for compatibility) ``abc.ResourceReader``. Any - loader that implements (implicitly or explicitly) the - ``TraversableResources.files`` method will be capable of - supplying resources with subdirectory support. Closes #77. -* Preferred way to access ``as_file`` is now from top-level module. - ``importlib_resources.trees.as_file`` is deprecated and discouraged. - Closes #86. -* Moved ``Traversable`` abc to ``abc`` module. Closes #87. - -v1.2.0 -====== - -* Traversable now requires an ``open`` method. Closes #81. -* Fixed error on ``Python 3.5.{0,3}``. Closes #83. -* Updated packaging to resolve version from package metadata. - Closes #82. - -v1.1.0 -====== - -* Add support for retrieving resources from subdirectories of packages - through the new ``files()`` function, which returns a ``Traversable`` - object with ``joinpath`` and ``read_*`` interfaces matching those - of ``pathlib.Path`` objects. This new function supersedes all of the - previous functionality as it provides a more general-purpose access - to a package's resources. - - With this function, subdirectories are supported (Closes #58). - - The - documentation has been updated to reflect that this function is now - the preferred interface for loading package resources. It does not, - however, support resources from arbitrary loaders. It currently only - supports resources from file system path and zipfile packages (a - consequence of the ResourceReader interface only operating on - Python packages). - -1.0.2 -===== - -* Fix ``setup_requires`` and ``install_requires`` metadata in ``setup.cfg``. - Given by Anthony Sottile. - -1.0.1 -===== - -* Update Trove classifiers. Closes #63 - -1.0 -=== - -* Backport fix for test isolation from Python 3.8/3.7. Closes #61 - -0.8 -=== - -* Strip ``importlib_resources.__version__``. Closes #56 -* Fix a metadata problem with older setuptools. Closes #57 -* Add an ``__all__`` to ``importlib_resources``. Closes #59 - -0.7 -=== - -* Fix ``setup.cfg`` metadata bug. Closes #55 - -0.6 -=== - -* Move everything from ``pyproject.toml`` to ``setup.cfg``, with the added - benefit of fixing the PyPI metadata. Closes #54 -* Turn off mypy's ``strict_optional`` setting for now. - -0.5 -=== - -* Resynchronize with Python 3.7; changes the return type of ``contents()`` to - be an ``Iterable``. Closes #52 - -0.4 -=== - -* Correctly find resources in subpackages inside a zip file. Closes #51 - -0.3 -=== - -* The API, implementation, and documentation is synchronized with the Python - 3.7 standard library. Closes #47 -* When run under Python 3.7 this API shadows the stdlib versions. Closes #50 - -0.2 -=== - -* **Backward incompatible change**. Split the ``open()`` and ``read()`` calls - into separate binary and text versions, i.e. ``open_binary()``, - ``open_text()``, ``read_binary()``, and ``read_text()``. Closes #41 -* Fix a bug where unrelated resources could be returned from ``contents()``. - Closes #44 -* Correctly prevent namespace packages from containing resources. Closes #20 - -0.1 -=== - -* Initial release. - - -.. - Local Variables: - mode: change-log-mode - indent-tabs-mode: nil - sentence-end-double-space: t - fill-column: 78 - coding: utf-8 - End: diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 378b991a..00000000 --- a/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -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 - -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/importlib_resources/_adapters.py b/Lib/importlib/_adapters.py similarity index 100% rename from importlib_resources/_adapters.py rename to Lib/importlib/_adapters.py diff --git a/importlib_resources/_common.py b/Lib/importlib/_common.py similarity index 98% rename from importlib_resources/_common.py rename to Lib/importlib/_common.py index d3223bdd..549fee37 100644 --- a/importlib_resources/_common.py +++ b/Lib/importlib/_common.py @@ -9,7 +9,7 @@ from typing import Union, Any, Optional from .abc import ResourceReader, Traversable -from ._compat import wrap_spec +from ._adapters import wrap_spec Package = Union[types.ModuleType, str] diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py new file mode 100644 index 00000000..0b4a3f80 --- /dev/null +++ b/Lib/importlib/abc.py @@ -0,0 +1,442 @@ +"""Abstract base classes related to import.""" +from . import _bootstrap_external +from . import machinery +try: + import _frozen_importlib +except ImportError as exc: + if exc.name != '_frozen_importlib': + raise + _frozen_importlib = None +try: + import _frozen_importlib_external +except ImportError: + _frozen_importlib_external = _bootstrap_external +from ._abc import Loader +import abc +import warnings +from typing import BinaryIO, Iterable, Text +from typing import Protocol, runtime_checkable + + +def _register(abstract_cls, *classes): + for cls in classes: + abstract_cls.register(cls) + if _frozen_importlib is not None: + try: + frozen_cls = getattr(_frozen_importlib, cls.__name__) + except AttributeError: + frozen_cls = getattr(_frozen_importlib_external, cls.__name__) + abstract_cls.register(frozen_cls) + + +class Finder(metaclass=abc.ABCMeta): + + """Legacy abstract base class for import finders. + + It may be subclassed for compatibility with legacy third party + reimplementations of the import system. Otherwise, finder + implementations should derive from the more specific MetaPathFinder + or PathEntryFinder ABCs. + + Deprecated since Python 3.3 + """ + + def __init__(self): + warnings.warn("the Finder ABC is deprecated and " + "slated for removal in Python 3.12; use MetaPathFinder " + "or PathEntryFinder instead", + DeprecationWarning) + + @abc.abstractmethod + def find_module(self, fullname, path=None): + """An abstract method that should find a module. + The fullname is a str and the optional path is a str or None. + Returns a Loader object or None. + """ + warnings.warn("importlib.abc.Finder along with its find_module() " + "method are deprecated and " + "slated for removal in Python 3.12; use " + "MetaPathFinder.find_spec() or " + "PathEntryFinder.find_spec() instead", + DeprecationWarning) + + +class MetaPathFinder(metaclass=abc.ABCMeta): + + """Abstract base class for import finders on sys.meta_path.""" + + # We don't define find_spec() here since that would break + # hasattr checks we do to support backward compatibility. + + def find_module(self, fullname, path): + """Return a loader for the module. + + If no module is found, return None. The fullname is a str and + the path is a list of strings or None. + + This method is deprecated since Python 3.4 in favor of + finder.find_spec(). If find_spec() exists then backwards-compatible + functionality is provided for this method. + + """ + warnings.warn("MetaPathFinder.find_module() is deprecated since Python " + "3.4 in favor of MetaPathFinder.find_spec() and is " + "slated for removal in Python 3.12", + DeprecationWarning, + stacklevel=2) + if not hasattr(self, 'find_spec'): + return None + found = self.find_spec(fullname, path) + return found.loader if found is not None else None + + def invalidate_caches(self): + """An optional method for clearing the finder's cache, if any. + This method is used by importlib.invalidate_caches(). + """ + +_register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter, + machinery.PathFinder, machinery.WindowsRegistryFinder) + + +class PathEntryFinder(metaclass=abc.ABCMeta): + + """Abstract base class for path entry finders used by PathFinder.""" + + # We don't define find_spec() here since that would break + # hasattr checks we do to support backward compatibility. + + def find_loader(self, fullname): + """Return (loader, namespace portion) for the path entry. + + The fullname is a str. The namespace portion is a sequence of + path entries contributing to part of a namespace package. The + sequence may be empty. If loader is not None, the portion will + be ignored. + + The portion will be discarded if another path entry finder + locates the module as a normal module or package. + + This method is deprecated since Python 3.4 in favor of + finder.find_spec(). If find_spec() is provided than backwards-compatible + functionality is provided. + """ + warnings.warn("PathEntryFinder.find_loader() is deprecated since Python " + "3.4 in favor of PathEntryFinder.find_spec() " + "(available since 3.4)", + DeprecationWarning, + stacklevel=2) + if not hasattr(self, 'find_spec'): + return None, [] + found = self.find_spec(fullname) + if found is not None: + if not found.submodule_search_locations: + portions = [] + else: + portions = found.submodule_search_locations + return found.loader, portions + else: + return None, [] + + find_module = _bootstrap_external._find_module_shim + + def invalidate_caches(self): + """An optional method for clearing the finder's cache, if any. + This method is used by PathFinder.invalidate_caches(). + """ + +_register(PathEntryFinder, machinery.FileFinder) + + +class ResourceLoader(Loader): + + """Abstract base class for loaders which can return data from their + back-end storage. + + This ABC represents one of the optional protocols specified by PEP 302. + + """ + + @abc.abstractmethod + def get_data(self, path): + """Abstract method which when implemented should return the bytes for + the specified path. The path must be a str.""" + raise OSError + + +class InspectLoader(Loader): + + """Abstract base class for loaders which support inspection about the + modules they can load. + + This ABC represents one of the optional protocols specified by PEP 302. + + """ + + def is_package(self, fullname): + """Optional method which when implemented should return whether the + module is a package. The fullname is a str. Returns a bool. + + Raises ImportError if the module cannot be found. + """ + raise ImportError + + def get_code(self, fullname): + """Method which returns the code object for the module. + + The fullname is a str. Returns a types.CodeType if possible, else + returns None if a code object does not make sense + (e.g. built-in module). Raises ImportError if the module cannot be + found. + """ + source = self.get_source(fullname) + if source is None: + return None + return self.source_to_code(source) + + @abc.abstractmethod + def get_source(self, fullname): + """Abstract method which should return the source code for the + module. The fullname is a str. Returns a str. + + Raises ImportError if the module cannot be found. + """ + raise ImportError + + @staticmethod + def source_to_code(data, path=''): + """Compile 'data' into a code object. + + The 'data' argument can be anything that compile() can handle. The'path' + argument should be where the data was retrieved (when applicable).""" + return compile(data, path, 'exec', dont_inherit=True) + + exec_module = _bootstrap_external._LoaderBasics.exec_module + load_module = _bootstrap_external._LoaderBasics.load_module + +_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter) + + +class ExecutionLoader(InspectLoader): + + """Abstract base class for loaders that wish to support the execution of + modules as scripts. + + This ABC represents one of the optional protocols specified in PEP 302. + + """ + + @abc.abstractmethod + def get_filename(self, fullname): + """Abstract method which should return the value that __file__ is to be + set to. + + Raises ImportError if the module cannot be found. + """ + raise ImportError + + def get_code(self, fullname): + """Method to return the code object for fullname. + + Should return None if not applicable (e.g. built-in module). + Raise ImportError if the module cannot be found. + """ + source = self.get_source(fullname) + if source is None: + return None + try: + path = self.get_filename(fullname) + except ImportError: + return self.source_to_code(source) + else: + return self.source_to_code(source, path) + +_register(ExecutionLoader, machinery.ExtensionFileLoader) + + +class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader): + + """Abstract base class partially implementing the ResourceLoader and + ExecutionLoader ABCs.""" + +_register(FileLoader, machinery.SourceFileLoader, + machinery.SourcelessFileLoader) + + +class SourceLoader(_bootstrap_external.SourceLoader, ResourceLoader, ExecutionLoader): + + """Abstract base class for loading source code (and optionally any + corresponding bytecode). + + To support loading from source code, the abstractmethods inherited from + ResourceLoader and ExecutionLoader need to be implemented. To also support + loading from bytecode, the optional methods specified directly by this ABC + is required. + + Inherited abstractmethods not implemented in this ABC: + + * ResourceLoader.get_data + * ExecutionLoader.get_filename + + """ + + def path_mtime(self, path): + """Return the (int) modification time for the path (str).""" + if self.path_stats.__func__ is SourceLoader.path_stats: + raise OSError + return int(self.path_stats(path)['mtime']) + + def path_stats(self, path): + """Return a metadata dict for the source pointed to by the path (str). + Possible keys: + - 'mtime' (mandatory) is the numeric timestamp of last source + code modification; + - 'size' (optional) is the size in bytes of the source code. + """ + if self.path_mtime.__func__ is SourceLoader.path_mtime: + raise OSError + return {'mtime': self.path_mtime(path)} + + def set_data(self, path, data): + """Write the bytes to the path (if possible). + + Accepts a str path and data as bytes. + + Any needed intermediary directories are to be created. If for some + reason the file cannot be written because of permissions, fail + silently. + """ + +_register(SourceLoader, machinery.SourceFileLoader) + + +class ResourceReader(metaclass=abc.ABCMeta): + """Abstract base class for loaders to provide resource reading support.""" + + @abc.abstractmethod + def open_resource(self, resource: Text) -> BinaryIO: + """Return an opened, file-like object for binary reading. + + The 'resource' argument is expected to represent only a file name. + If the resource cannot be found, FileNotFoundError is raised. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def resource_path(self, resource: Text) -> Text: + """Return the file system path to the specified resource. + + The 'resource' argument is expected to represent only a file name. + If the resource does not exist on the file system, raise + FileNotFoundError. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def is_resource(self, path: Text) -> bool: + """Return True if the named 'path' is a resource. + + Files are resources, directories are not. + """ + raise FileNotFoundError + + @abc.abstractmethod + def contents(self) -> Iterable[str]: + """Return an iterable of entries in `package`.""" + raise FileNotFoundError + + +@runtime_checkable +class Traversable(Protocol): + """ + An object with a subset of pathlib.Path methods suitable for + traversing directories and opening files. + """ + + @abc.abstractmethod + def iterdir(self): + """ + Yield Traversable objects in self + """ + + def read_bytes(self): + """ + Read contents of self as bytes + """ + with self.open('rb') as strm: + return strm.read() + + def read_text(self, encoding=None): + """ + Read contents of self as text + """ + with self.open(encoding=encoding) as strm: + return strm.read() + + @abc.abstractmethod + def is_dir(self) -> bool: + """ + Return True if self is a dir + """ + + @abc.abstractmethod + def is_file(self) -> bool: + """ + Return True if self is a file + """ + + @abc.abstractmethod + def joinpath(self, child): + """ + Return Traversable child in self + """ + + def __truediv__(self, child): + """ + Return Traversable child in self + """ + return self.joinpath(child) + + @abc.abstractmethod + def open(self, mode='r', *args, **kwargs): + """ + mode may be 'r' or 'rb' to open as text or binary. Return a handle + suitable for reading (same as pathlib.Path.open). + + When opening as text, accepts encoding parameters such as those + accepted by io.TextIOWrapper. + """ + + @abc.abstractproperty + def name(self) -> str: + """ + The base name of this object without any parent references. + """ + + +class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + + @abc.abstractmethod + def files(self): + """Return a Traversable object for the loaded package.""" + + def open_resource(self, resource): + return self.files().joinpath(resource).open('rb') + + def resource_path(self, resource): + raise FileNotFoundError(resource) + + def is_resource(self, path): + return self.files().joinpath(path).is_file() + + def contents(self): + return (item.name for item in self.files().iterdir()) diff --git a/importlib_resources/readers.py b/Lib/importlib/readers.py similarity index 97% rename from importlib_resources/readers.py rename to Lib/importlib/readers.py index c918ef82..41089c07 100644 --- a/importlib_resources/readers.py +++ b/Lib/importlib/readers.py @@ -1,10 +1,8 @@ import collections +import zipfile import pathlib - from . import abc -from ._compat import ZipPath - def remove_duplicates(items): return iter(collections.OrderedDict.fromkeys(items)) @@ -45,7 +43,7 @@ def is_resource(self, path): return target.is_file() and target.exists() def files(self): - return ZipPath(self.archive, self.prefix) + return zipfile.Path(self.archive, self.prefix) class MultiplexedPath(abc.Traversable): diff --git a/importlib_resources/_py3.py b/Lib/importlib/resources.py similarity index 93% rename from importlib_resources/_py3.py rename to Lib/importlib/resources.py index 17cc8a47..8a98663f 100644 --- a/importlib_resources/_py3.py +++ b/Lib/importlib/resources.py @@ -2,6 +2,8 @@ import io from . import _common +from ._common import as_file, files +from .abc import ResourceReader from contextlib import suppress from importlib.abc import ResourceLoader from importlib.machinery import ModuleSpec @@ -14,6 +16,23 @@ from collections.abc import Sequence from functools import singledispatch + +__all__ = [ + 'Package', + 'Resource', + 'ResourceReader', + 'as_file', + 'contents', + 'files', + 'is_resource', + 'open_binary', + 'open_text', + 'path', + 'read_binary', + 'read_text', +] + + Package = Union[str, ModuleType] Resource = Union[str, os.PathLike] @@ -124,7 +143,7 @@ def _path_from_open_resource(reader, resource): def is_resource(package: Package, name: str) -> bool: - """True if `name` is a resource inside `package`. + """True if 'name' is a resource inside 'package'. Directories are *not* resources. """ @@ -140,7 +159,7 @@ def is_resource(package: Package, name: str) -> bool: def contents(package: Package) -> Iterable[str]: - """Return an iterable of entries in `package`. + """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 diff --git a/Lib/test/test_importlib/__init__.py b/Lib/test/test_importlib/__init__.py new file mode 100644 index 00000000..4b16ecc3 --- /dev/null +++ b/Lib/test/test_importlib/__init__.py @@ -0,0 +1,5 @@ +import os +from test.support import load_package_tests + +def load_tests(*args): + return load_package_tests(os.path.dirname(__file__), *args) diff --git a/importlib_resources/tests/__init__.py b/Lib/test/test_importlib/data01/__init__.py similarity index 100% rename from importlib_resources/tests/__init__.py rename to Lib/test/test_importlib/data01/__init__.py diff --git a/importlib_resources/tests/data01/binary.file b/Lib/test/test_importlib/data01/binary.file similarity index 100% rename from importlib_resources/tests/data01/binary.file rename to Lib/test/test_importlib/data01/binary.file diff --git a/importlib_resources/tests/data01/__init__.py b/Lib/test/test_importlib/data01/subdirectory/__init__.py similarity index 100% rename from importlib_resources/tests/data01/__init__.py rename to Lib/test/test_importlib/data01/subdirectory/__init__.py diff --git a/importlib_resources/tests/data01/subdirectory/binary.file b/Lib/test/test_importlib/data01/subdirectory/binary.file similarity index 100% rename from importlib_resources/tests/data01/subdirectory/binary.file rename to Lib/test/test_importlib/data01/subdirectory/binary.file diff --git a/importlib_resources/tests/data01/utf-16.file b/Lib/test/test_importlib/data01/utf-16.file similarity index 100% rename from importlib_resources/tests/data01/utf-16.file rename to Lib/test/test_importlib/data01/utf-16.file diff --git a/importlib_resources/tests/data01/utf-8.file b/Lib/test/test_importlib/data01/utf-8.file similarity index 100% rename from importlib_resources/tests/data01/utf-8.file rename to Lib/test/test_importlib/data01/utf-8.file diff --git a/importlib_resources/tests/data01/subdirectory/__init__.py b/Lib/test/test_importlib/data02/__init__.py similarity index 100% rename from importlib_resources/tests/data01/subdirectory/__init__.py rename to Lib/test/test_importlib/data02/__init__.py diff --git a/importlib_resources/tests/data02/__init__.py b/Lib/test/test_importlib/data02/one/__init__.py similarity index 100% rename from importlib_resources/tests/data02/__init__.py rename to Lib/test/test_importlib/data02/one/__init__.py diff --git a/importlib_resources/tests/data02/one/resource1.txt b/Lib/test/test_importlib/data02/one/resource1.txt similarity index 100% rename from importlib_resources/tests/data02/one/resource1.txt rename to Lib/test/test_importlib/data02/one/resource1.txt diff --git a/importlib_resources/tests/data02/one/__init__.py b/Lib/test/test_importlib/data02/two/__init__.py similarity index 100% rename from importlib_resources/tests/data02/one/__init__.py rename to Lib/test/test_importlib/data02/two/__init__.py diff --git a/importlib_resources/tests/data02/two/resource2.txt b/Lib/test/test_importlib/data02/two/resource2.txt similarity index 100% rename from importlib_resources/tests/data02/two/resource2.txt rename to Lib/test/test_importlib/data02/two/resource2.txt diff --git a/importlib_resources/tests/data02/two/__init__.py b/Lib/test/test_importlib/data03/__init__.py similarity index 100% rename from importlib_resources/tests/data02/two/__init__.py rename to Lib/test/test_importlib/data03/__init__.py diff --git a/importlib_resources/tests/zipdata01/__init__.py b/Lib/test/test_importlib/data03/namespace/portion1/__init__.py similarity index 100% rename from importlib_resources/tests/zipdata01/__init__.py rename to Lib/test/test_importlib/data03/namespace/portion1/__init__.py diff --git a/importlib_resources/tests/zipdata02/__init__.py b/Lib/test/test_importlib/data03/namespace/portion2/__init__.py similarity index 100% rename from importlib_resources/tests/zipdata02/__init__.py rename to Lib/test/test_importlib/data03/namespace/portion2/__init__.py diff --git a/importlib_resources/py.typed b/Lib/test/test_importlib/data03/namespace/resource1.txt similarity index 100% rename from importlib_resources/py.typed rename to Lib/test/test_importlib/data03/namespace/resource1.txt diff --git a/importlib_resources/tests/namespacedata01/binary.file b/Lib/test/test_importlib/namespacedata01/binary.file similarity index 100% rename from importlib_resources/tests/namespacedata01/binary.file rename to Lib/test/test_importlib/namespacedata01/binary.file diff --git a/importlib_resources/tests/namespacedata01/utf-16.file b/Lib/test/test_importlib/namespacedata01/utf-16.file similarity index 100% rename from importlib_resources/tests/namespacedata01/utf-16.file rename to Lib/test/test_importlib/namespacedata01/utf-16.file diff --git a/importlib_resources/tests/namespacedata01/utf-8.file b/Lib/test/test_importlib/namespacedata01/utf-8.file similarity index 100% rename from importlib_resources/tests/namespacedata01/utf-8.file rename to Lib/test/test_importlib/namespacedata01/utf-8.file diff --git a/importlib_resources/tests/test_files.py b/Lib/test/test_importlib/test_files.py similarity index 91% rename from importlib_resources/tests/test_files.py rename to Lib/test/test_importlib/test_files.py index d701d81f..481829b7 100644 --- a/importlib_resources/tests/test_files.py +++ b/Lib/test/test_importlib/test_files.py @@ -1,8 +1,8 @@ import typing import unittest -import importlib_resources as resources -from importlib_resources.abc import Traversable +from importlib import resources +from importlib.abc import Traversable from . import data01 from . import util diff --git a/importlib_resources/tests/test_open.py b/Lib/test/test_importlib/test_open.py similarity index 87% rename from importlib_resources/tests/test_open.py rename to Lib/test/test_importlib/test_open.py index 919fc0db..b75675f4 100644 --- a/importlib_resources/tests/test_open.py +++ b/Lib/test/test_importlib/test_open.py @@ -1,17 +1,17 @@ import unittest -import importlib_resources as resources +from importlib import resources from . import data01 from . import util -class CommonBinaryTests(util.CommonTests, unittest.TestCase): +class CommonBinaryTests(util.CommonResourceTests, unittest.TestCase): def execute(self, package, path): with resources.open_binary(package, path): pass -class CommonTextTests(util.CommonTests, unittest.TestCase): +class CommonTextTests(util.CommonResourceTests, unittest.TestCase): def execute(self, package, path): with resources.open_text(package, path): pass @@ -19,9 +19,9 @@ def execute(self, package, path): class OpenTests: def test_open_binary(self): - 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 resources.open_text(self.data, 'utf-8.file') as fp: diff --git a/importlib_resources/tests/test_path.py b/Lib/test/test_importlib/test_path.py similarity index 86% rename from importlib_resources/tests/test_path.py rename to Lib/test/test_importlib/test_path.py index 9180626f..d6ed09a9 100644 --- a/importlib_resources/tests/test_path.py +++ b/Lib/test/test_importlib/test_path.py @@ -1,12 +1,12 @@ import io import unittest -import importlib_resources as resources +from importlib import resources from . import data01 from . import util -class CommonTests(util.CommonTests, unittest.TestCase): +class CommonTests(util.CommonResourceTests, unittest.TestCase): def execute(self, package, path): with resources.path(package, path): pass @@ -29,11 +29,9 @@ class PathDiskTests(PathTests, unittest.TestCase): data = data01 def test_natural_path(self): - """ - Guarantee the internal implementation detail that - file-system-backed resources do not get the tempdir - treatment. - """ + # Guarantee the internal implementation detail that + # 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) diff --git a/importlib_resources/tests/test_read.py b/Lib/test/test_importlib/test_read.py similarity index 90% rename from importlib_resources/tests/test_read.py rename to Lib/test/test_importlib/test_read.py index 5422eea3..f6ec13af 100644 --- a/importlib_resources/tests/test_read.py +++ b/Lib/test/test_importlib/test_read.py @@ -1,17 +1,16 @@ import unittest -import importlib_resources as resources +from importlib import import_module, resources from . import data01 from . import util -from importlib import import_module -class CommonBinaryTests(util.CommonTests, unittest.TestCase): +class CommonBinaryTests(util.CommonResourceTests, unittest.TestCase): def execute(self, package, path): resources.read_binary(package, path) -class CommonTextTests(util.CommonTests, unittest.TestCase): +class CommonTextTests(util.CommonResourceTests, unittest.TestCase): def execute(self, package, path): resources.read_text(package, path) diff --git a/importlib_resources/tests/test_reader.py b/Lib/test/test_importlib/test_reader.py similarity index 98% rename from importlib_resources/tests/test_reader.py rename to Lib/test/test_importlib/test_reader.py index 16841a50..9d20c976 100644 --- a/importlib_resources/tests/test_reader.py +++ b/Lib/test/test_importlib/test_reader.py @@ -4,7 +4,7 @@ import unittest from importlib import import_module -from importlib_resources.readers import MultiplexedPath, NamespaceReader +from importlib.readers import MultiplexedPath, NamespaceReader class MultiplexedPathTest(unittest.TestCase): diff --git a/importlib_resources/tests/test_resource.py b/Lib/test/test_importlib/test_resource.py similarity index 98% rename from importlib_resources/tests/test_resource.py rename to Lib/test/test_importlib/test_resource.py index 75748e65..003f7e95 100644 --- a/importlib_resources/tests/test_resource.py +++ b/Lib/test/test_importlib/test_resource.py @@ -1,14 +1,14 @@ 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, unlink +from importlib import resources, import_module +from test.support import import_helper +from test.support.os_helper import unlink class ResourceTests: diff --git a/importlib_resources/tests/update-zips.py b/Lib/test/test_importlib/update-zips.py similarity index 100% rename from importlib_resources/tests/update-zips.py rename to Lib/test/test_importlib/update-zips.py diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py new file mode 100644 index 00000000..ca0d8c9b --- /dev/null +++ b/Lib/test/test_importlib/util.py @@ -0,0 +1,582 @@ +import abc +import builtins +import contextlib +import errno +import functools +import importlib +from importlib import machinery, util, invalidate_caches +from importlib.abc import ResourceReader +import io +import marshal +import os +import os.path +from pathlib import Path, PurePath +from test import support +from test.support import import_helper +from test.support import os_helper +import unittest +import sys +import tempfile +import types + +from . import data01 +from . import zipdata01 + + +BUILTINS = types.SimpleNamespace() +BUILTINS.good_name = None +BUILTINS.bad_name = None +if 'errno' in sys.builtin_module_names: + BUILTINS.good_name = 'errno' +if 'importlib' not in sys.builtin_module_names: + BUILTINS.bad_name = 'importlib' + +EXTENSIONS = types.SimpleNamespace() +EXTENSIONS.path = None +EXTENSIONS.ext = None +EXTENSIONS.filename = None +EXTENSIONS.file_path = None +EXTENSIONS.name = '_testcapi' + +def _extension_details(): + global EXTENSIONS + for path in sys.path: + for ext in machinery.EXTENSION_SUFFIXES: + filename = EXTENSIONS.name + ext + file_path = os.path.join(path, filename) + if os.path.exists(file_path): + EXTENSIONS.path = path + EXTENSIONS.ext = ext + EXTENSIONS.filename = filename + EXTENSIONS.file_path = file_path + return + +_extension_details() + + +def import_importlib(module_name): + """Import a module from importlib both w/ and w/o _frozen_importlib.""" + fresh = ('importlib',) if '.' in module_name else () + frozen = import_helper.import_fresh_module(module_name) + source = import_helper.import_fresh_module(module_name, fresh=fresh, + blocked=('_frozen_importlib', '_frozen_importlib_external')) + return {'Frozen': frozen, 'Source': source} + + +def specialize_class(cls, kind, base=None, **kwargs): + # XXX Support passing in submodule names--load (and cache) them? + # That would clean up the test modules a bit more. + if base is None: + base = unittest.TestCase + elif not isinstance(base, type): + base = base[kind] + name = '{}_{}'.format(kind, cls.__name__) + bases = (cls, base) + specialized = types.new_class(name, bases) + specialized.__module__ = cls.__module__ + specialized._NAME = cls.__name__ + specialized._KIND = kind + for attr, values in kwargs.items(): + value = values[kind] + setattr(specialized, attr, value) + return specialized + + +def split_frozen(cls, base=None, **kwargs): + frozen = specialize_class(cls, 'Frozen', base, **kwargs) + source = specialize_class(cls, 'Source', base, **kwargs) + return frozen, source + + +def test_both(test_class, base=None, **kwargs): + return split_frozen(test_class, base, **kwargs) + + +CASE_INSENSITIVE_FS = True +# Windows is the only OS that is *always* case-insensitive +# (OS X *can* be case-sensitive). +if sys.platform not in ('win32', 'cygwin'): + changed_name = __file__.upper() + if changed_name == __file__: + changed_name = __file__.lower() + if not os.path.exists(changed_name): + CASE_INSENSITIVE_FS = False + +source_importlib = import_importlib('importlib')['Source'] +__import__ = {'Frozen': staticmethod(builtins.__import__), + 'Source': staticmethod(source_importlib.__import__)} + + +def case_insensitive_tests(test): + """Class decorator that nullifies tests requiring a case-insensitive + file system.""" + return unittest.skipIf(not CASE_INSENSITIVE_FS, + "requires a case-insensitive filesystem")(test) + + +def submodule(parent, name, pkg_dir, content=''): + path = os.path.join(pkg_dir, name + '.py') + with open(path, 'w', encoding='utf-8') as subfile: + subfile.write(content) + return '{}.{}'.format(parent, name), path + + +def get_code_from_pyc(pyc_path): + """Reads a pyc file and returns the unmarshalled code object within. + + No header validation is performed. + """ + with open(pyc_path, 'rb') as pyc_f: + pyc_f.seek(16) + return marshal.load(pyc_f) + + +@contextlib.contextmanager +def uncache(*names): + """Uncache a module from sys.modules. + + A basic sanity check is performed to prevent uncaching modules that either + cannot/shouldn't be uncached. + + """ + for name in names: + if name in ('sys', 'marshal', 'imp'): + raise ValueError( + "cannot uncache {0}".format(name)) + try: + del sys.modules[name] + except KeyError: + pass + try: + yield + finally: + for name in names: + try: + del sys.modules[name] + except KeyError: + pass + + +@contextlib.contextmanager +def temp_module(name, content='', *, pkg=False): + conflicts = [n for n in sys.modules if n.partition('.')[0] == name] + with os_helper.temp_cwd(None) as cwd: + with uncache(name, *conflicts): + with import_helper.DirsOnSysPath(cwd): + invalidate_caches() + + location = os.path.join(cwd, name) + if pkg: + modpath = os.path.join(location, '__init__.py') + os.mkdir(name) + else: + modpath = location + '.py' + if content is None: + # Make sure the module file gets created. + content = '' + if content is not None: + # not a namespace package + with open(modpath, 'w', encoding='utf-8') as modfile: + modfile.write(content) + yield location + + +@contextlib.contextmanager +def import_state(**kwargs): + """Context manager to manage the various importers and stored state in the + sys module. + + The 'modules' attribute is not supported as the interpreter state stores a + pointer to the dict that the interpreter uses internally; + reassigning to sys.modules does not have the desired effect. + + """ + originals = {} + try: + for attr, default in (('meta_path', []), ('path', []), + ('path_hooks', []), + ('path_importer_cache', {})): + originals[attr] = getattr(sys, attr) + if attr in kwargs: + new_value = kwargs[attr] + del kwargs[attr] + else: + new_value = default + setattr(sys, attr, new_value) + if len(kwargs): + raise ValueError( + 'unrecognized arguments: {0}'.format(kwargs.keys())) + yield + finally: + for attr, value in originals.items(): + setattr(sys, attr, value) + + +class _ImporterMock: + + """Base class to help with creating importer mocks.""" + + def __init__(self, *names, module_code={}): + self.modules = {} + self.module_code = {} + for name in names: + if not name.endswith('.__init__'): + import_name = name + else: + import_name = name[:-len('.__init__')] + if '.' not in name: + package = None + elif import_name == name: + package = name.rsplit('.', 1)[0] + else: + package = import_name + module = types.ModuleType(import_name) + module.__loader__ = self + module.__file__ = '' + module.__package__ = package + module.attr = name + if import_name != name: + module.__path__ = [''] + self.modules[import_name] = module + if import_name in module_code: + self.module_code[import_name] = module_code[import_name] + + def __getitem__(self, name): + return self.modules[name] + + def __enter__(self): + self._uncache = uncache(*self.modules.keys()) + self._uncache.__enter__() + return self + + def __exit__(self, *exc_info): + self._uncache.__exit__(None, None, None) + + +class mock_modules(_ImporterMock): + + """Importer mock using PEP 302 APIs.""" + + def find_module(self, fullname, path=None): + if fullname not in self.modules: + return None + else: + return self + + def load_module(self, fullname): + if fullname not in self.modules: + raise ImportError + else: + sys.modules[fullname] = self.modules[fullname] + if fullname in self.module_code: + try: + self.module_code[fullname]() + except Exception: + del sys.modules[fullname] + raise + return self.modules[fullname] + + +class mock_spec(_ImporterMock): + + """Importer mock using PEP 451 APIs.""" + + def find_spec(self, fullname, path=None, parent=None): + try: + module = self.modules[fullname] + except KeyError: + return None + spec = util.spec_from_file_location( + fullname, module.__file__, loader=self, + submodule_search_locations=getattr(module, '__path__', None)) + return spec + + def create_module(self, spec): + if spec.name not in self.modules: + raise ImportError + return self.modules[spec.name] + + def exec_module(self, module): + try: + self.module_code[module.__spec__.name]() + except KeyError: + pass + + +def writes_bytecode_files(fxn): + """Decorator to protect sys.dont_write_bytecode from mutation and to skip + tests that require it to be set to False.""" + if sys.dont_write_bytecode: + return lambda *args, **kwargs: None + @functools.wraps(fxn) + def wrapper(*args, **kwargs): + original = sys.dont_write_bytecode + sys.dont_write_bytecode = False + try: + to_return = fxn(*args, **kwargs) + finally: + sys.dont_write_bytecode = original + return to_return + return wrapper + + +def ensure_bytecode_path(bytecode_path): + """Ensure that the __pycache__ directory for PEP 3147 pyc file exists. + + :param bytecode_path: File system path to PEP 3147 pyc file. + """ + try: + os.mkdir(os.path.dirname(bytecode_path)) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + +@contextlib.contextmanager +def temporary_pycache_prefix(prefix): + """Adjust and restore sys.pycache_prefix.""" + _orig_prefix = sys.pycache_prefix + sys.pycache_prefix = prefix + try: + yield + finally: + sys.pycache_prefix = _orig_prefix + + +@contextlib.contextmanager +def create_modules(*names): + """Temporarily create each named module with an attribute (named 'attr') + that contains the name passed into the context manager that caused the + creation of the module. + + All files are created in a temporary directory returned by + tempfile.mkdtemp(). This directory is inserted at the beginning of + sys.path. When the context manager exits all created files (source and + bytecode) are explicitly deleted. + + No magic is performed when creating packages! This means that if you create + a module within a package you must also create the package's __init__ as + well. + + """ + source = 'attr = {0!r}' + created_paths = [] + mapping = {} + state_manager = None + uncache_manager = None + try: + temp_dir = tempfile.mkdtemp() + mapping['.root'] = temp_dir + import_names = set() + for name in names: + if not name.endswith('__init__'): + import_name = name + else: + import_name = name[:-len('.__init__')] + import_names.add(import_name) + if import_name in sys.modules: + del sys.modules[import_name] + name_parts = name.split('.') + file_path = temp_dir + for directory in name_parts[:-1]: + file_path = os.path.join(file_path, directory) + if not os.path.exists(file_path): + os.mkdir(file_path) + created_paths.append(file_path) + file_path = os.path.join(file_path, name_parts[-1] + '.py') + with open(file_path, 'w', encoding='utf-8') as file: + file.write(source.format(name)) + created_paths.append(file_path) + mapping[name] = file_path + uncache_manager = uncache(*import_names) + uncache_manager.__enter__() + state_manager = import_state(path=[temp_dir]) + state_manager.__enter__() + yield mapping + finally: + if state_manager is not None: + state_manager.__exit__(None, None, None) + if uncache_manager is not None: + uncache_manager.__exit__(None, None, None) + os_helper.rmtree(temp_dir) + + +def mock_path_hook(*entries, importer): + """A mock sys.path_hooks entry.""" + def hook(entry): + if entry not in entries: + raise ImportError + return importer + return hook + + +class CASEOKTestBase: + + def caseok_env_changed(self, *, should_exist): + possibilities = b'PYTHONCASEOK', 'PYTHONCASEOK' + if any(x in self.importlib._bootstrap_external._os.environ + for x in possibilities) != should_exist: + self.skipTest('os.environ changes not reflected in _os.environ') + + +def create_package(file, path, is_package=True, contents=()): + class Reader(ResourceReader): + def get_resource_reader(self, package): + return self + + def open_resource(self, path): + self._path = path + if isinstance(file, Exception): + raise file + else: + return file + + def resource_path(self, path_): + self._path = path_ + if isinstance(path, Exception): + raise path + else: + return path + + def is_resource(self, path_): + self._path = path_ + if isinstance(path, Exception): + raise path + for entry in contents: + parts = entry.split('/') + if len(parts) == 1 and parts[0] == path_: + return True + return False + + def contents(self): + if isinstance(path, Exception): + raise path + # There's no yield from in baseball, er, Python 2. + for entry in contents: + yield entry + + name = 'testingpackage' + # Unfortunately importlib.util.module_from_spec() was not introduced until + # Python 3.5. + module = types.ModuleType(name) + loader = Reader() + spec = machinery.ModuleSpec( + name, loader, + origin='does-not-exist', + is_package=is_package) + module.__spec__ = spec + module.__loader__ = loader + return module + + +class CommonResourceTests(abc.ABC): + @abc.abstractmethod + def execute(self, package, path): + raise NotImplementedError + + def test_package_name(self): + # 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. + self.execute(data01, '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) + + @unittest.skipIf(sys.version_info < (3, 6), 'requires os.PathLike support') + def test_pathlib_path(self): + # Passing in a pathlib.PurePath object for the path should succeed. + 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 relative 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__] + 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['test.test_importlib.util'] + self.execute(module, 'utf-8.file') + + @unittest.skipIf(sys.version_info < (3,), 'No ResourceReader in Python 2') + def test_resource_opener(self): + 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') + + @unittest.skipIf(sys.version_info < (3,), 'No ResourceReader in Python 2') + def test_resource_path(self): + bytes_data = io.BytesIO(b'Hello, world!') + path = __file__ + package = create_package(file=bytes_data, path=path) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_useless_loader(self): + package = create_package(file=FileNotFoundError(), + path=FileNotFoundError()) + with self.assertRaises(FileNotFoundError): + self.execute(package, 'utf-8.file') + + +class ZipSetupBase: + ZIP_MODULE = None + + @classmethod + def setUpClass(cls): + data_path = 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 + + def setUp(self): + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + +class ZipSetup(ZipSetupBase): + ZIP_MODULE = zipdata01 # type: ignore diff --git a/Lib/test/test_importlib/zipdata01/__init__.py b/Lib/test/test_importlib/zipdata01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_resources/tests/zipdata01/ziptestdata.zip b/Lib/test/test_importlib/zipdata01/ziptestdata.zip similarity index 100% rename from importlib_resources/tests/zipdata01/ziptestdata.zip rename to Lib/test/test_importlib/zipdata01/ziptestdata.zip diff --git a/Lib/test/test_importlib/zipdata02/__init__.py b/Lib/test/test_importlib/zipdata02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_resources/tests/zipdata02/ziptestdata.zip b/Lib/test/test_importlib/zipdata02/ziptestdata.zip similarity index 100% rename from importlib_resources/tests/zipdata02/ziptestdata.zip rename to Lib/test/test_importlib/zipdata02/ziptestdata.zip diff --git a/README.rst b/README.rst deleted file mode 100644 index 65b799e3..00000000 --- a/README.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. image:: https://img.shields.io/pypi/v/importlib_resources.svg - :target: `PyPI link`_ - -.. image:: https://img.shields.io/pypi/pyversions/importlib_resources.svg - :target: `PyPI link`_ - -.. _PyPI link: https://pypi.org/project/importlib_resources - -.. image:: https://github.com/python/importlib_resources/workflows/tests/badge.svg - :target: https://github.com/python/importlib_resources/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/importlib-resources/badge/?version=latest - :target: https://importlib-resources.readthedocs.io/en/latest/?badge=latest - -``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. - -The key goal of this module is to replace parts of `pkg_resources -`_ with a -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. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 66c7f4bd..00000000 --- a/codecov.yml +++ /dev/null @@ -1,2 +0,0 @@ -codecov: - token: 5eb1bc45-1b7f-43e6-8bc1-f2b02833dba9 diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index d24ee04b..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] - -master_doc = "index" - -link_files = { - '../CHANGES.rst': dict( - using=dict(GH='https://github.com'), - replace=[ - dict( - pattern=r'(Issue #|\B#)(?P\d+)', - url='{package_url}/issues/{issue}', - ), - dict( - pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', - with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', - ), - dict( - pattern=r'PEP[- ](?P\d+)', - url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', - ), - dict( - pattern=r'(Python #|bpo-)(?P\d+)', - url='http://bugs.python.org/issue{python}', - ), - ], - ), -} diff --git a/docs/history.rst b/docs/history.rst deleted file mode 100644 index 8e217503..00000000 --- a/docs/history.rst +++ /dev/null @@ -1,8 +0,0 @@ -:tocdepth: 2 - -.. _changes: - -History -******* - -.. include:: ../CHANGES (links).rst diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 82cb007a..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,45 +0,0 @@ -Welcome to |project| documentation! -=================================== - -``importlib_resources`` is a library which provides for access to *resources* -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 -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. - -The documentation here includes a general :ref:`usage ` guide and a -:ref:`migration ` guide for projects that want to adopt -``importlib_resources`` instead of ``pkg_resources``. - - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - using.rst - migration.rst - history.rst - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :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 diff --git a/docs/migration.rst b/docs/migration.rst deleted file mode 100644 index 04d059cc..00000000 --- a/docs/migration.rst +++ /dev/null @@ -1,155 +0,0 @@ -.. _migration: - -================= - Migration guide -================= - -The following guide will help you migrate common ``pkg_resources`` APIs to -``importlib_resources``. Only a small number of the most common APIs are -supported by ``importlib_resources``, so projects that use other features -(e.g. entry points) will have to find other solutions. -``importlib_resources`` primarily supports the following `basic resource -access`_ APIs: - -* ``pkg_resources.resource_filename()`` -* ``pkg_resources.resource_stream()`` -* ``pkg_resources.resource_string()`` -* ``pkg_resources.resource_listdir()`` -* ``pkg_resources.resource_isdir()`` - -Note that although the steps below provide a drop-in replacement for the -above methods, for many use-cases, a better approach is to use the -``Traversable`` path from ``files()`` directly. - - -pkg_resources.resource_filename() -================================= - -``resource_filename()`` is one of the more interesting APIs because it -guarantees that the return value names a file on the file system. This means -that if the resource is in a zip file, ``pkg_resources`` will extract the -file and return the name of the temporary file it created. The problem is -that ``pkg_resources`` also *implicitly* cleans up this temporary file, -without control over its lifetime by the programmer. - -``importlib_resources`` takes a different approach. Its equivalent API is the -``files()`` function, which returns a Traversable object implementing a -subset of the -:py:class:`pathlib.Path` interface suitable for reading the contents and -provides a wrapper for creating a temporary file on the system in a -context whose lifetime is managed by the user. Note though -that if the resource is *already* on the file system, ``importlib_resources`` -still returns a context manager, but nothing needs to get cleaned up. - -Here's an example from ``pkg_resources``:: - - path = pkg_resources.resource_filename('my.package', 'resource.dat') - -The best way to convert this is with the following idiom:: - - ref = importlib_resources.files('my.package') / 'resource.dat' - with importlib_resources.as_file(ref) as path: - # Do something with path. After the with-statement exits, any - # temporary file created will be immediately cleaned up. - -That's all fine if you only need the file temporarily, but what if you need it -to stick around for a while? One way of doing this is to use an -:py:class:`contextlib.ExitStack` instance and manage the resource explicitly:: - - from contextlib import ExitStack - file_manager = ExitStack() - ref = importlib_resources.files('my.package') / 'resource.dat' - path = file_manager.enter_context( - importlib_resources.as_file(ref)) - -Now ``path`` will continue to exist until you explicitly call -``file_manager.close()``. What if you want the file to exist until the -process exits, or you can't pass ``file_manager`` around in your code? Use an -:py:mod:`atexit` handler:: - - import atexit - file_manager = ExitStack() - atexit.register(file_manager.close) - ref = importlib_resources.files('my.package') / 'resource.dat' - path = file_manager.enter_context( - importlib_resources.as_file(ref)) - -Assuming your Python interpreter exits gracefully, the temporary file will be -cleaned up when Python exits. - - -pkg_resources.resource_stream() -=============================== - -``pkg_resources.resource_stream()`` returns a readable file-like object opened -in binary mode. When you read from the returned file-like object, you get -bytes. E.g.:: - - with pkg_resources.resource_stream('my.package', 'resource.dat') as fp: - my_bytes = fp.read() - -The equivalent code in ``importlib_resources`` is pretty straightforward:: - - ref = importlib_resources.files('my.package').joinpath('resource.dat') - with ref.open() as fp: - my_bytes = fp.read() - - -pkg_resources.resource_string() -=============================== - -In Python 2, ``pkg_resources.resource_string()`` returns the contents of a -resource as a ``str``. In Python 3, this function is a misnomer; it actually -returns the contents of the named resource as ``bytes``. That's why the -following example is often written for clarity as:: - - from pkg_resources import resource_string as resource_bytes - contents = resource_bytes('my.package', 'resource.dat') - -This can be easily rewritten like so:: - - ref = importlib_resources.files('my.package').joinpath('resource.dat') - contents = f.read_bytes() - - -pkg_resources.resource_listdir() -================================ - -This function lists the entries in the package, both files and directories, -but it does not recurse into subdirectories, e.g.:: - - for entry in pkg_resources.resource_listdir('my.package', 'subpackage'): - print(entry) - -This is easily rewritten using the following idiom:: - - for entry in importlib_resources.files('my.package.subpackage').iterdir(): - print(entry.name) - -Note: - -* ``Traversable.iterdir()`` returns *all* the entries in the - subpackage, i.e. both resources (files) and non-resources (directories). -* ``Traversable.iterdir()`` returns additional traversable objects, which if - directories can also be iterated over (recursively). -* ``Traversable.iterdir()``, like ``pathlib.Path`` returns an iterator, not a - concrete sequence. -* The order in which the elements are returned is undefined. - - -pkg_resources.resource_isdir() -============================== - -You can ask ``pkg_resources`` to tell you whether a particular resource inside -a package is a directory or not:: - - if pkg_resources.resource_isdir('my.package', 'resource'): - print('A directory') - -The ``importlib_resources`` equivalent is straightforward:: - - if importlib_resources.files('my.package').joinpath('resource').isdir(): - print('A directory') - - -.. _`basic resource access`: http://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access diff --git a/docs/using.rst b/docs/using.rst deleted file mode 100644 index c0a60ad1..00000000 --- a/docs/using.rst +++ /dev/null @@ -1,197 +0,0 @@ -.. _using: - -=========================== - Using importlib_resources -=========================== - -``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. - -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 -keep in mind that this is just a metaphor. Resources and packages **do not** -have to exist as physical files and directories on the file system. - -If you have a file system layout such as:: - - data/ - __init__.py - one/ - __init__.py - resource1.txt - resources1/ - resource1.1.txt - two/ - __init__.py - resource2.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 -``__init__.py`` files [#fn1]_. That means that in Python, all of these import -statements work:: - - import data - import data.one - from data import two - -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]_. - -In this analogy then, resources are just files or directories contained in a -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. - - -Example -======= - -Let's say you are writing an email parsing library and in your test suite you -have a sample email message in a file called ``message.eml``. You would like -to access the contents of this file for your tests, so you put this in your -project under the ``email/tests/data/message.eml`` path. Let's say your unit -tests live in ``email/tests/test_email.py``. - -Your test could read the data file by doing something like:: - - data_dir = os.path.join(os.path.dirname(__file__), 'tests', 'data') - data_path = os.path.join(data_dir, 'message.eml') - with open(data_path, encoding='utf-8') as fp: - eml = fp.read() - -But there's a problem with this! The use of ``__file__`` doesn't work if your -package lives inside a zip file, since in that case this code does not live on -the file system. - -You could use the `pkg_resources API`_ like so:: - - # In Python 3, resource_string() actually returns bytes! - from pkg_resources import resource_string as resource_bytes - eml = resource_bytes('email.tests.data', 'message.eml').decode('utf-8') - -This requires you to make Python packages of both ``email/tests`` and -``email/tests/data``, by placing an empty ``__init__.py`` files in each of -those directories. - -The problem with the ``pkg_resources`` approach is that, depending on the -packages in your environment, ``pkg_resources`` can be expensive -just to import. This behavior -can have a serious negative impact on things like command line startup time -for Python implement commands. - -``importlib_resources`` solves this performance challenge by being built -entirely on the back of the -stdlib :py:mod:`importlib`. By taking advantage of all the efficiencies in -Python's import system, and the fact that it's built into Python, using -``importlib_resources`` can be much more performant. The equivalent code -using ``importlib_resources`` would look like:: - - from importlib_resources import files - # Reads contents with UTF-8 encoding and returns str. - eml = files('email.tests.data').joinpath('message.eml').read_text() - - -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 -passed in, it must name an importable Python package, and this is first -imported. Thus the above example could also be written as:: - - import email.tests.data - eml = files(email.tests.data).joinpath('message.eml').read_text() - - -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. -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:: - - from importlib_resources import files, as_file - - source = files(email.tests.data).joinpath('message.eml') - 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. - -.. 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. - - -Extending -========= - -Starting with Python 3.9 and ``importlib_resources`` 2.0, this package -provides an interface for non-standard loaders, such as those used by -executable bundlers, to supply resources. These loaders should supply a -``get_resource_reader`` method, which is passed a module name and -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 - 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 - 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 diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py deleted file mode 100644 index ea4c7efe..00000000 --- a/importlib_resources/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Read resources contained within a package.""" - -from ._common import ( - as_file, - files, -) - -from importlib_resources._py3 import ( - Package, - Resource, - contents, - is_resource, - open_binary, - open_text, - path, - read_binary, - read_text, -) -from importlib_resources.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/_compat.py b/importlib_resources/_compat.py deleted file mode 100644 index deb071b9..00000000 --- a/importlib_resources/_compat.py +++ /dev/null @@ -1,87 +0,0 @@ -# flake8: noqa - -import abc -import sys -import pathlib -from contextlib import suppress - -try: - from zipfile import Path as ZipPath # type: ignore -except ImportError: - 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 - - -class TraversableResourcesLoader: - """ - Adapt loaders to provide TraversableResources and other - compatibility. - """ - - def __init__(self, spec): - self.spec = spec - - @property - 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) - - 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): - if pathlib.Path(self.path).exists(): - 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 - # local NamespaceReader if a namespace module - _namespace_reader(self.spec) - or - # local FileReader - _file_reader(self.spec) - or _adapters.DegenerateFiles(self.spec) - ) - - -def wrap_spec(package): - """ - Construct a package spec with traversable compatibility - on the spec/loader/reader. - """ - from . import _adapters - - return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py deleted file mode 100644 index 56dc8127..00000000 --- a/importlib_resources/abc.py +++ /dev/null @@ -1,137 +0,0 @@ -import abc -from typing import BinaryIO, Iterable, Text - -from ._compat import runtime_checkable, Protocol - - -class ResourceReader(metaclass=abc.ABCMeta): - """Abstract base class for loaders to provide resource reading support.""" - - @abc.abstractmethod - def open_resource(self, resource: Text) -> BinaryIO: - """Return an opened, file-like object for binary reading. - - The 'resource' argument is expected to represent only a file name. - If the resource cannot be found, FileNotFoundError is raised. - """ - # This deliberately raises FileNotFoundError instead of - # NotImplementedError so that if this method is accidentally called, - # it'll still do the right thing. - raise FileNotFoundError - - @abc.abstractmethod - def resource_path(self, resource: Text) -> Text: - """Return the file system path to the specified resource. - - The 'resource' argument is expected to represent only a file name. - If the resource does not exist on the file system, raise - FileNotFoundError. - """ - # This deliberately raises FileNotFoundError instead of - # NotImplementedError so that if this method is accidentally called, - # it'll still do the right thing. - raise FileNotFoundError - - @abc.abstractmethod - def is_resource(self, path: Text) -> bool: - """Return True if the named 'path' is a resource. - - Files are resources, directories are not. - """ - raise FileNotFoundError - - @abc.abstractmethod - def contents(self) -> Iterable[str]: - """Return an iterable of entries in `package`.""" - raise FileNotFoundError - - -@runtime_checkable -class Traversable(Protocol): - """ - An object with a subset of pathlib.Path methods suitable for - traversing directories and opening files. - """ - - @abc.abstractmethod - def iterdir(self): - """ - Yield Traversable objects in self - """ - - def read_bytes(self): - """ - Read contents of self as bytes - """ - with self.open('rb') as strm: - return strm.read() - - def read_text(self, encoding=None): - """ - Read contents of self as text - """ - with self.open(encoding=encoding) as strm: - return strm.read() - - @abc.abstractmethod - def is_dir(self) -> bool: - """ - Return True if self is a dir - """ - - @abc.abstractmethod - def is_file(self) -> bool: - """ - Return True if self is a file - """ - - @abc.abstractmethod - def joinpath(self, child): - """ - Return Traversable child in self - """ - - def __truediv__(self, child): - """ - Return Traversable child in self - """ - return self.joinpath(child) - - @abc.abstractmethod - def open(self, mode='r', *args, **kwargs): - """ - mode may be 'r' or 'rb' to open as text or binary. Return a handle - suitable for reading (same as pathlib.Path.open). - - When opening as text, accepts encoding parameters such as those - accepted by io.TextIOWrapper. - """ - - @abc.abstractproperty - def name(self) -> str: - """ - The base name of this object without any parent references. - """ - - -class TraversableResources(ResourceReader): - """ - The required interface for providing traversable - resources. - """ - - @abc.abstractmethod - def files(self): - """Return a Traversable object for the loaded package.""" - - def open_resource(self, resource): - return self.files().joinpath(resource).open('rb') - - def resource_path(self, resource): - raise FileNotFoundError(resource) - - def is_resource(self, path): - return self.files().joinpath(path).is_file() - - def contents(self): - return (item.name for item in self.files().iterdir()) diff --git a/importlib_resources/tests/_compat.py b/importlib_resources/tests/_compat.py deleted file mode 100644 index 4c99cffd..00000000 --- a/importlib_resources/tests/_compat.py +++ /dev/null @@ -1,19 +0,0 @@ -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, modules_cleanup - - -try: - # Python 3.10 - from test.support.os_helper import unlink -except ImportError: - from test.support import unlink as _unlink - - def unlink(target): - return _unlink(os.fspath(target)) diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py deleted file mode 100644 index 3247bcf1..00000000 --- a/importlib_resources/tests/util.py +++ /dev/null @@ -1,170 +0,0 @@ -import abc -import importlib -import io -import sys -import types -from pathlib import Path, PurePath - -from . import data01 -from . import zipdata01 -from ..abc import ResourceReader -from ._compat import import_helper - - -from importlib.machinery import ModuleSpec - - -class Reader(ResourceReader): - def __init__(self, **kwargs): - vars(self).update(kwargs) - - def get_resource_reader(self, package): - return self - - def open_resource(self, path): - self._path = path - if isinstance(self.file, Exception): - raise self.file - else: - return self.file - - def resource_path(self, path_): - self._path = path_ - if isinstance(self.path, Exception): - raise self.path - else: - 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 contents(self): - if isinstance(self.path, Exception): - raise self.path - yield from self._contents - - -def create_package(file, path, is_package=True, contents=()): - 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 - - -class CommonTests(metaclass=abc.ABCMeta): - @abc.abstractmethod - def execute(self, package, path): - raise NotImplementedError - - def test_package_name(self): - # 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. - self.execute(data01, '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) - - def test_pathlib_path(self): - # Passing in a pathlib.PurePath object for the path should succeed. - 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__] - 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_resource_opener(self): - 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): - bytes_data = io.BytesIO(b'Hello, world!') - path = __file__ - package = create_package(file=bytes_data, path=path) - self.execute(package, 'utf-8.file') - self.assertEqual(package.__loader__._path, 'utf-8.file') - - def test_useless_loader(self): - package = create_package(file=FileNotFoundError(), path=FileNotFoundError()) - with self.assertRaises(FileNotFoundError): - self.execute(package, 'utf-8.file') - - -class ZipSetupBase: - ZIP_MODULE = None - - @classmethod - def setUpClass(cls): - data_path = 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 - - def setUp(self): - modules = import_helper.modules_setup() - self.addCleanup(import_helper.modules_cleanup, *modules) - - -class ZipSetup(ZipSetupBase): - ZIP_MODULE = zipdata01 # type: ignore diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 976ba029..00000000 --- a/mypy.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index b6ebc0be..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.1"] -build-backend = "setuptools.build_meta" - -[tool.black] -skip-string-normalization = true - -[tool.setuptools_scm] - -[pytest.enabler.black] -addopts = "--black" - -[pytest.enabler.mypy] -addopts = "--mypy" - -[pytest.enabler.flake8] -addopts = "--flake8" - -[pytest.enabler.cov] -addopts = "--cov" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index d7f0b115..00000000 --- a/pytest.ini +++ /dev/null @@ -1,9 +0,0 @@ -[pytest] -norecursedirs=dist build .tox .eggs -addopts=--doctest-modules -doctest_optionflags=ALLOW_UNICODE ELLIPSIS -# workaround for warning pytest-dev/pytest#6178 -junit_family=xunit2 -filterwarnings= - # https://github.com/pytest-dev/pytest/issues/6928 - ignore:direct construction of .*Item has been deprecated:DeprecationWarning diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7d9424d0..00000000 --- a/setup.cfg +++ /dev/null @@ -1,53 +0,0 @@ -[metadata] -license_files = - LICENSE -name = importlib_resources -author = Barry Warsaw -author_email = barry@python.org -description = Read resources from Python packages -long_description = file: README.rst -url = https://github.com/python/importlib_resources -license = Apache2 -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] -packages = find_namespace: -include_package_data = true -python_requires = >=3.6 -install_requires = - zipp >= 0.4; python_version < '3.8' -setup_requires = setuptools_scm[toml] >= 3.4.1 - -[options.packages.find] -exclude = - build* - docs* - tests* - -[options.extras_require] -testing = - # upstream - pytest >= 3.5, !=3.7.3 - pytest-checkdocs >= 1.2.3 - pytest-flake8 - pytest-black >= 0.3.7; python_implementation != "PyPy" - pytest-cov - pytest-mypy; python_implementation != "PyPy" - pytest-enabler - - # local - -docs = - # upstream - sphinx - jaraco.packaging >= 8.2 - rst.linker >= 1.9 - - # local 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() diff --git a/skeleton.md b/skeleton.md deleted file mode 100644 index 0938f892..00000000 --- a/skeleton.md +++ /dev/null @@ -1,166 +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/). - -## 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 -``` diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 01828205..00000000 --- a/tox.ini +++ /dev/null @@ -1,48 +0,0 @@ -[tox] -envlist = python -minversion = 3.2 -# https://github.com/jaraco/skeleton/issues/6 -tox_pip_extensions_ext_venv_update = true -toxworkdir={env:TOX_WORK_DIR:.tox} - - -[testenv] -deps = -commands = - pytest {posargs} -usedevelop = True -extras = testing - -[testenv:docs] -extras = - docs - testing -changedir = docs -commands = - python -m sphinx . {toxinidir}/build/html - -[testenv: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:release] -skip_install = True -deps = - build - twine[keyring]>=1.13 - path - jaraco.develop>=7.1 -passenv = - TWINE_PASSWORD - GITHUB_TOKEN -setenv = - TWINE_USERNAME = {env:TWINE_USERNAME:__token__} -commands = - python -c "import path; path.Path('dist').rmtree_p()" - python -m build - python -m twine upload dist/* - python -m jaraco.develop.create-github-release