diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69..9c01fc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ on: # required if branches-ignore is supplied (jaraco/skeleton#103) - '**' pull_request: + workflow_dispatch: permissions: contents: read @@ -34,23 +35,25 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.8" - - "3.12" + - "3.9" + - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.9" - platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest + - python: "3.12" + platform: ubuntu-latest + - python: "3.14" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 - name: Setup Python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ec58e2..04870d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.7.1 hooks: - id: ruff args: [--fix, --unsafe-fixes] diff --git a/NEWS.rst b/NEWS.rst index 36aac37..c3612c1 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v6.5.0 +====== + +Features +-------- + +- Add type annotations for Traversable.open. (#317) +- Require Python 3.9 or later. + + v6.4.5 ====== diff --git a/README.rst b/README.rst index b302f62..4f9fa02 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ .. image:: https://readthedocs.org/projects/importlib-resources/badge/?version=latest :target: https://importlib-resources.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2024-informational +.. image:: https://img.shields.io/badge/skeleton-2025-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/importlib-resources diff --git a/docs/conf.py b/docs/conf.py index 64ea3d5..570346b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', @@ -28,8 +27,12 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(python/cpython#|Python #|bpo-)(?P\d+)', - url='http://bugs.python.org/issue{python}', + pattern=r'(python/cpython#|Python #)(?P\d+)', + url='https://github.com/python/cpython/issues/{python}', + ), + dict( + pattern=r'bpo-(?P\d+)', + url='http://bugs.python.org/issue{bpo}', ), ], ), diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index 723c9f9..27d6c7f 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -8,12 +8,11 @@ """ from ._common import ( + Anchor, + Package, as_file, files, - Package, - Anchor, ) - from ._functional import ( contents, is_resource, @@ -23,10 +22,8 @@ read_binary, read_text, ) - from .abc import ResourceReader - __all__ = [ 'Package', 'Anchor', diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index f065d49..5f41c26 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -1,15 +1,15 @@ +import contextlib +import functools +import importlib +import inspect +import itertools import os import pathlib import tempfile -import functools -import contextlib import types -import importlib -import inspect import warnings -import itertools +from typing import Optional, Union, cast -from typing import Union, Optional, cast from .abc import ResourceReader, Traversable Package = Union[types.ModuleType, str] diff --git a/importlib_resources/_functional.py b/importlib_resources/_functional.py index f59416f..5a3ca0a 100644 --- a/importlib_resources/_functional.py +++ b/importlib_resources/_functional.py @@ -2,8 +2,7 @@ import warnings -from ._common import files, as_file - +from ._common import as_file, files _MISSING = object() diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 7a58dd2..84631c7 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,12 +1,25 @@ import abc -import io import itertools +import os import pathlib -from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional -from typing import runtime_checkable, Protocol - -from .compat.py38 import StrPath - +from typing import ( + Any, + BinaryIO, + Iterable, + Iterator, + NoReturn, + Optional, + Protocol, + Text, + TextIO, + Union, + overload, + runtime_checkable, +) + +from typing_extensions import Literal + +StrPath = Union[str, os.PathLike[str]] __all__ = ["ResourceReader", "Traversable", "TraversableResources"] @@ -130,8 +143,16 @@ def __truediv__(self, child: StrPath) -> "Traversable": """ return self.joinpath(child) + @overload + def open(self, mode: Literal['r'] = 'r', *args: Any, **kwargs: Any) -> TextIO: ... + + @overload + def open(self, mode: Literal['rb'], *args: Any, **kwargs: Any) -> BinaryIO: ... + @abc.abstractmethod - def open(self, mode='r', *args, **kwargs): + def open( + self, mode: str = 'r', *args: Any, **kwargs: Any + ) -> Union[TextIO, BinaryIO]: """ mode may be 'r' or 'rb' to open as text or binary. Return a handle suitable for reading (same as pathlib.Path.open). @@ -158,7 +179,7 @@ class TraversableResources(ResourceReader): def files(self) -> "Traversable": """Return a Traversable object for the loaded package.""" - def open_resource(self, resource: StrPath) -> io.BufferedReader: + def open_resource(self, resource: StrPath) -> BinaryIO: return self.files().joinpath(resource).open('rb') def resource_path(self, resource: Any) -> NoReturn: diff --git a/importlib_resources/compat/py38.py b/importlib_resources/compat/py38.py deleted file mode 100644 index 4d54825..0000000 --- a/importlib_resources/compat/py38.py +++ /dev/null @@ -1,11 +0,0 @@ -import os -import sys - -from typing import Union - - -if sys.version_info >= (3, 9): - StrPath = Union[str, os.PathLike[str]] -else: - # PathLike is only subscriptable at runtime in 3.9+ - StrPath = Union[str, "os.PathLike[str]"] diff --git a/importlib_resources/compat/py39.py b/importlib_resources/compat/py39.py index ed5abd5..684d3c6 100644 --- a/importlib_resources/compat/py39.py +++ b/importlib_resources/compat/py39.py @@ -1,6 +1,5 @@ import sys - __all__ = ['ZipPath'] diff --git a/importlib_resources/future/adapters.py b/importlib_resources/future/adapters.py index 0e9764b..239e52b 100644 --- a/importlib_resources/future/adapters.py +++ b/importlib_resources/future/adapters.py @@ -3,7 +3,7 @@ from contextlib import suppress from types import SimpleNamespace -from .. import readers, _adapters +from .. import _adapters, readers def _block_standard(reader_getter): @@ -23,6 +23,13 @@ def wrapper(*args, **kwargs): except NotADirectoryError: # MultiplexedPath may fail on zip subdirectory return + except ValueError as exc: + # NamespaceReader in stdlib may fail for editable installs + # (python/importlib_resources#311, python/importlib_resources#318) + # Remove after bugfix applied to Python 3.13. + if "not enough values to unpack" not in str(exc): + raise + return # Python 3.10+ mod_name = reader.__class__.__module__ if mod_name.startswith('importlib.') and mod_name.endswith('readers'): diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index 4f761c6..99884b6 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -3,14 +3,13 @@ import collections import contextlib import itertools -import pathlib import operator +import pathlib import re import warnings from collections.abc import Iterator from . import abc - from ._itertools import only from .compat.py39 import ZipPath diff --git a/importlib_resources/tests/_path.py b/importlib_resources/tests/_path.py index b144628..0033983 100644 --- a/importlib_resources/tests/_path.py +++ b/importlib_resources/tests/_path.py @@ -1,10 +1,6 @@ -import pathlib import functools - -from typing import Dict, Union -from typing import runtime_checkable -from typing import Protocol - +import pathlib +from typing import Dict, Protocol, Union, runtime_checkable #### # from jaraco.path 3.7.1 diff --git a/importlib_resources/tests/test_compatibilty_files.py b/importlib_resources/tests/test_compatibilty_files.py index 13ad0df..e8aac28 100644 --- a/importlib_resources/tests/test_compatibilty_files.py +++ b/importlib_resources/tests/test_compatibilty_files.py @@ -2,7 +2,6 @@ import unittest import importlib_resources as resources - from importlib_resources._adapters import ( CompatibilityFiles, wrap_spec, diff --git a/importlib_resources/tests/test_contents.py b/importlib_resources/tests/test_contents.py index 741a740..dcb872e 100644 --- a/importlib_resources/tests/test_contents.py +++ b/importlib_resources/tests/test_contents.py @@ -1,4 +1,5 @@ import unittest + import importlib_resources as resources from . import util diff --git a/importlib_resources/tests/test_custom.py b/importlib_resources/tests/test_custom.py index 86c6567..25ae0e7 100644 --- a/importlib_resources/tests/test_custom.py +++ b/importlib_resources/tests/test_custom.py @@ -1,10 +1,11 @@ -import unittest import contextlib import pathlib +import unittest import importlib_resources as resources + from .. import abc -from ..abc import TraversableResources, ResourceReader +from ..abc import ResourceReader, TraversableResources from . import util from .compat.py39 import os_helper diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index f1fe233..be20660 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,17 +1,16 @@ -import os +import contextlib +import importlib import pathlib import py_compile -import shutil import textwrap import unittest import warnings -import importlib -import contextlib import importlib_resources as resources + from ..abc import Traversable from . import util -from .compat.py39 import os_helper, import_helper +from .compat.py39 import import_helper, os_helper @contextlib.contextmanager @@ -72,7 +71,7 @@ def test_non_paths_in_dunder_path(self): to cause the ``PathEntryFinder`` to be called when searching for packages. In that case, resources should still be loadable. """ - import namespacedata01 + import namespacedata01 # type: ignore[import-not-found] namespacedata01.__path__.append( '__editable__.sample_namespace-1.0.finder.__path_hook__' @@ -154,18 +153,19 @@ def test_implicit_files_submodule(self): def _compile_importlib(self): """ Make a compiled-only copy of the importlib resources package. + + Currently only code is copied, as importlib resources doesn't itself + have any resources. """ bin_site = self.fixtures.enter_context(os_helper.temp_dir()) c_resources = pathlib.Path(bin_site, 'c_resources') sources = pathlib.Path(resources.__file__).parent - shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__']) - - for dirpath, _, filenames in os.walk(c_resources): - for filename in filenames: - source_path = pathlib.Path(dirpath) / filename - cfile = source_path.with_suffix('.pyc') - py_compile.compile(source_path, cfile) - pathlib.Path.unlink(source_path) + + for source_path in sources.glob('**/*.py'): + c_path = c_resources.joinpath(source_path.relative_to(sources)).with_suffix( + '.pyc' + ) + py_compile.compile(source_path, c_path) self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): diff --git a/importlib_resources/tests/test_functional.py b/importlib_resources/tests/test_functional.py index 1851edf..2285389 100644 --- a/importlib_resources/tests/test_functional.py +++ b/importlib_resources/tests/test_functional.py @@ -1,12 +1,11 @@ -import unittest -import os import importlib - -from .compat.py39 import warnings_helper +import os +import unittest import importlib_resources as resources from . import util +from .compat.py39 import warnings_helper # Since the functional API forwards to Traversable, we only test # filesystem resources here -- not zip files, namespace packages etc. @@ -182,17 +181,23 @@ def test_contents(self): set(c), {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'}, ) - with self.assertRaises(OSError), warnings_helper.check_warnings(( - ".*contents.*", - DeprecationWarning, - )): + with ( + self.assertRaises(OSError), + warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )), + ): list(resources.contents(self.anchor01, 'utf-8.file')) for path_parts in self._gen_resourcetxt_path_parts(): - with self.assertRaises(OSError), warnings_helper.check_warnings(( - ".*contents.*", - DeprecationWarning, - )): + with ( + self.assertRaises(OSError), + warnings_helper.check_warnings(( + ".*contents.*", + DeprecationWarning, + )), + ): list(resources.contents(self.anchor01, *path_parts)) with warnings_helper.check_warnings((".*contents.*", DeprecationWarning)): c = resources.contents(self.anchor01, 'subdirectory') diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index c40bb8c..8a4b68e 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -1,6 +1,7 @@ import unittest import importlib_resources as resources + from . import util diff --git a/importlib_resources/tests/test_path.py b/importlib_resources/tests/test_path.py index 1e30f2b..0be673d 100644 --- a/importlib_resources/tests/test_path.py +++ b/importlib_resources/tests/test_path.py @@ -3,6 +3,7 @@ import unittest import importlib_resources as resources + from . import util diff --git a/importlib_resources/tests/test_read.py b/importlib_resources/tests/test_read.py index 6780a2d..216c8fe 100644 --- a/importlib_resources/tests/test_read.py +++ b/importlib_resources/tests/test_read.py @@ -1,8 +1,9 @@ import unittest +from importlib import import_module + import importlib_resources as resources from . import util -from importlib import import_module class CommonBinaryTests(util.CommonTests, unittest.TestCase): diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 0a77eb4..f8cfd8d 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -1,8 +1,8 @@ import os.path import pathlib import unittest - from importlib import import_module + from importlib_resources.readers import MultiplexedPath, NamespaceReader from . import util diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index a0da6a3..c80afdc 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -1,8 +1,9 @@ import unittest +from importlib import import_module + import importlib_resources as resources from . import util -from importlib import import_module class ResourceTests: diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index a4eafac..07d1529 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -1,18 +1,16 @@ import abc +import contextlib import importlib import io +import pathlib import sys import types -import pathlib -import contextlib +from importlib.machinery import ModuleSpec from ..abc import ResourceReader -from .compat.py39 import import_helper, os_helper -from . import zip as zip_ from . import _path - - -from importlib.machinery import ModuleSpec +from . import zip as zip_ +from .compat.py39 import import_helper, os_helper class Reader(ResourceReader): @@ -145,7 +143,7 @@ def test_useless_loader(self): data01={ '__init__.py': '', 'binary.file': bytes(range(4)), - 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-16.file': '\ufeffHello, UTF-16 world!\n'.encode('utf-16-le'), 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), 'subdirectory': { '__init__.py': '', @@ -160,7 +158,7 @@ def test_useless_loader(self): }, namespacedata01={ 'binary.file': bytes(range(4)), - 'utf-16.file': 'Hello, UTF-16 world!\n'.encode('utf-16'), + 'utf-16.file': '\ufeffHello, UTF-16 world!\n'.encode('utf-16-le'), 'utf-8.file': 'Hello, UTF-8 world!\n'.encode('utf-8'), 'subdirectory': { 'binary.file': bytes(range(12, 16)), diff --git a/pyproject.toml b/pyproject.toml index ec5312d..608e751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "zipp >= 3.1.0; python_version < '3.10'", ] @@ -74,7 +74,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 diff --git a/ruff.toml b/ruff.toml index 922aa1f..9379d6e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,6 @@ +# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +extend = "pyproject.toml" + [lint] extend-select = [ "C901", diff --git a/tox.ini b/tox.ini index 01f0975..1424305 100644 --- a/tox.ini +++ b/tox.ini @@ -31,9 +31,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint \ - # workaround for sphinx-contrib/sphinx-lint#83 - --jobs 1 + python -m sphinxlint [testenv:finalize] description = assemble changelog and tag a release