diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4737659b..e97a7452 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ jobs: test: strategy: matrix: - python: [2.7, 3.6, 3.7, 3.8, 3.9] + python: [3.6, 3.7, 3.8, 3.9] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/coverage.ini b/coverage.ini index da82b3df..be7e32a4 100644 --- a/coverage.ini +++ b/coverage.ini @@ -6,7 +6,6 @@ omit = .tox/*/lib/python*/site-packages/* */tests/*.py */testing/*.py - importlib_resources/_py${OMIT}.py importlib_resources/__init__.py importlib_resources/_compat.py importlib_resources/abc.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 724203c5..f47c46cb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,11 @@ importlib_resources NEWS ========================== +v4.0.0 +====== + +* #108: Drop support for Python 2.7. Now requires Python 3.6+. + v3.3.1 ====== diff --git a/importlib_resources/__init__.py b/importlib_resources/__init__.py index f122f95e..da2047cd 100644 --- a/importlib_resources/__init__.py +++ b/importlib_resources/__init__.py @@ -1,11 +1,22 @@ """Read resources contained within a package.""" -import sys - 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 + # For compatibility. Ref #88. # Also requires hook-importlib_resources.py (Ref #101). __import__('importlib_resources.trees') @@ -25,29 +36,3 @@ 'read_binary', 'read_text', ] - - -if sys.version_info >= (3,): - 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 -else: - from importlib_resources._py2 import ( - contents, - is_resource, - open_binary, - open_text, - path, - read_binary, - read_text, - ) - del __all__[:3] diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index a7c2bf81..ffb0b3b0 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -1,15 +1,12 @@ -from __future__ import absolute_import - import os import tempfile import contextlib import types import importlib +from pathlib import Path +from functools import singledispatch -from ._compat import ( - Path, FileNotFoundError, - singledispatch, package_spec, - ) +from ._compat import package_spec if False: # TYPE_CHECKING from typing import Union, Any, Optional diff --git a/importlib_resources/_compat.py b/importlib_resources/_compat.py index 70b0f6b4..3c3f33bf 100644 --- a/importlib_resources/_compat.py +++ b/importlib_resources/_compat.py @@ -1,47 +1,9 @@ -from __future__ import absolute_import +import abc import sys +from contextlib import suppress # flake8: noqa -if sys.version_info > (3,5): - from pathlib import Path, PurePath -else: - from pathlib2 import Path, PurePath # type: ignore - - -if sys.version_info > (3,): - from contextlib import suppress -else: - from contextlib2 import suppress # type: ignore - - -try: - from functools import singledispatch -except ImportError: - from singledispatch import singledispatch # type: ignore - - -try: - from abc import ABC # type: ignore -except ImportError: - from abc import ABCMeta - - class ABC(object): # type: ignore - __metaclass__ = ABCMeta - - -try: - FileNotFoundError = FileNotFoundError # type: ignore -except NameError: - FileNotFoundError = OSError # type: ignore - - -try: - NotADirectoryError = NotADirectoryError # type: ignore -except NameError: - NotADirectoryError = OSError # type: ignore - - try: from zipfile import Path as ZipPath # type: ignore except ImportError: @@ -58,15 +20,7 @@ def runtime_checkable(cls): # type: ignore try: from typing import Protocol # type: ignore except ImportError: - Protocol = ABC # type: ignore - - -__metaclass__ = type - - -class PackageSpec: - def __init__(self, **kwargs): - vars(self).update(kwargs) + Protocol = abc.ABC # type: ignore class TraversableResourcesAdapter: @@ -88,7 +42,6 @@ def __init__(self, spec): @property def path(self): - # Python < 3 return self.spec.origin def get_resource_reader(self, name): @@ -129,11 +82,4 @@ def package_spec(package): matching the interfaces this library relies upon in later Python versions. """ - spec = getattr(package, '__spec__', None) or \ - PackageSpec( - origin=package.__file__, - loader=getattr(package, '__loader__', None), - name=package.__name__, - submodule_search_locations=getattr(package, '__path__', None), - ) - return TraversableResourcesAdapter(spec) + return TraversableResourcesAdapter(package.__spec__) diff --git a/importlib_resources/_py2.py b/importlib_resources/_py2.py deleted file mode 100644 index dd8c7d62..00000000 --- a/importlib_resources/_py2.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import errno - -from . import _common -from ._compat import FileNotFoundError -from io import BytesIO, TextIOWrapper, open as io_open - - -def open_binary(package, resource): - """Return a file-like object opened for binary reading of the resource.""" - resource = _common.normalize_path(resource) - package = _common.get_package(package) - # Using pathlib doesn't work well here due to the lack of 'strict' argument - # for pathlib.Path.resolve() prior to Python 3.6. - package_path = os.path.dirname(package.__file__) - relative_path = os.path.join(package_path, resource) - full_path = os.path.abspath(relative_path) - try: - return io_open(full_path, 'rb') - except IOError: - # This might be a package in a zip file. zipimport provides a loader - # with a functioning get_data() method, however we have to strip the - # archive (i.e. the .zip file's name) off the front of the path. This - # is because the zipimport loader in Python 2 doesn't actually follow - # PEP 302. It should allow the full path, but actually requires that - # the path be relative to the zip file. - try: - loader = package.__loader__ - full_path = relative_path[len(loader.archive)+1:] - data = loader.get_data(full_path) - except (IOError, AttributeError): - package_name = package.__name__ - message = '{!r} resource not found in {!r}'.format( - resource, package_name) - raise FileNotFoundError(message) - return BytesIO(data) - - -def open_text(package, resource, encoding='utf-8', errors='strict'): - """Return a file-like object opened for text reading of the resource.""" - return TextIOWrapper( - open_binary(package, resource), encoding=encoding, errors=errors) - - -def read_binary(package, resource): - """Return the binary contents of the resource.""" - with open_binary(package, resource) as fp: - return fp.read() - - -def read_text(package, resource, encoding='utf-8', errors='strict'): - """Return the decoded string of the resource. - - The decoding-related arguments have the same semantics as those of - bytes.decode(). - """ - with open_text(package, resource, encoding, errors) as fp: - return fp.read() - - -def path(package, resource): - """A context manager providing a file path object to the resource. - - If the resource does not already exist on its own on the file system, - a temporary file will be created. If the file was created, the file - will be deleted upon exiting the context manager (no exception is - raised if the file was deleted prior to the context manager - exiting). - """ - path = _common.files(package).joinpath(_common.normalize_path(resource)) - if not path.is_file(): - raise FileNotFoundError(path) - return _common.as_file(path) - - -def is_resource(package, name): - """True if name is a resource inside package. - - Directories are *not* resources. - """ - package = _common.get_package(package) - _common.normalize_path(name) - try: - package_contents = set(contents(package)) - except OSError as error: - if error.errno not in (errno.ENOENT, errno.ENOTDIR): - # We won't hit this in the Python 2 tests, so it'll appear - # uncovered. We could mock os.listdir() to return a non-ENOENT or - # ENOTDIR, but then we'd have to depend on another external - # library since Python 2 doesn't have unittest.mock. It's not - # worth it. - raise # pragma: nocover - return False - if name not in package_contents: - return False - return (_common.from_package(package) / name).is_file() - - -def contents(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 - to check if it is a resource or not. - """ - package = _common.get_package(package) - return list(item.name for item in _common.from_package(package).iterdir()) diff --git a/importlib_resources/abc.py b/importlib_resources/abc.py index 18bc4ef8..4d190c36 100644 --- a/importlib_resources/abc.py +++ b/importlib_resources/abc.py @@ -1,8 +1,6 @@ -from __future__ import absolute_import - import abc -from ._compat import ABC, FileNotFoundError, runtime_checkable, Protocol +from ._compat import runtime_checkable, Protocol # Use mypy's comment syntax for Python 2 compatibility try: @@ -11,7 +9,7 @@ pass -class ResourceReader(ABC): +class ResourceReader(metaclass=abc.ABCMeta): """Abstract base class for loaders to provide resource reading support.""" @abc.abstractmethod diff --git a/importlib_resources/readers.py b/importlib_resources/readers.py index ce9c0cae..38ffe097 100644 --- a/importlib_resources/readers.py +++ b/importlib_resources/readers.py @@ -1,11 +1,11 @@ import os.path from collections import OrderedDict +from pathlib import Path from . import abc -from ._compat import Path, ZipPath -from ._compat import FileNotFoundError, NotADirectoryError +from ._compat import ZipPath class FileReader(abc.TraversableResources): diff --git a/importlib_resources/tests/_compat.py b/importlib_resources/tests/_compat.py index edadf450..4c99cffd 100644 --- a/importlib_resources/tests/_compat.py +++ b/importlib_resources/tests/_compat.py @@ -1,23 +1,12 @@ -try: - from test.support import import_helper # type: ignore -except ImportError: - try: - # Python 3.9 and earlier - class import_helper: # type: ignore - from test.support import modules_setup, modules_cleanup - except ImportError: - from . import py27compat - - class import_helper: # type: ignore - modules_setup = staticmethod(py27compat.modules_setup) - modules_cleanup = staticmethod(py27compat.modules_cleanup) +import os try: - from os import fspath # type: ignore + from test.support import import_helper # type: ignore except ImportError: - # Python 3.5 - fspath = str # type: ignore + # Python 3.9 and earlier + class import_helper: # type: ignore + from test.support import modules_setup, modules_cleanup try: @@ -27,4 +16,4 @@ class import_helper: # type: ignore from test.support import unlink as _unlink def unlink(target): - return _unlink(fspath(target)) + return _unlink(os.fspath(target)) diff --git a/importlib_resources/tests/py27compat.py b/importlib_resources/tests/py27compat.py deleted file mode 100644 index 90c959e5..00000000 --- a/importlib_resources/tests/py27compat.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys - - -def modules_setup(): - return sys.modules.copy(), - - -def modules_cleanup(oldmodules): - # Encoders/decoders are registered permanently within the internal - # codec cache. If we destroy the corresponding modules their - # globals will be set to None which will trip up the cached functions. - encodings = [(k, v) for k, v in sys.modules.items() - if k.startswith('encodings.')] - sys.modules.clear() - sys.modules.update(encodings) - # XXX: This kind of problem can affect more than just encodings. - # In particular extension modules (such as _ssl) don't cope - # with reloading properly. Really, test modules should be cleaning - # out the test specific modules they know they added (ala test_runpy) - # rather than relying on this function (as test_importhooks and test_pkg - # do currently). Implicitly imported *real* modules should be left alone - # (see issue 10556). - sys.modules.update(oldmodules) diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 930570a0..9b35ffa6 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -4,7 +4,6 @@ import importlib_resources as resources from . import data01 from . import util -from .._compat import FileNotFoundError class CommonBinaryTests(util.CommonTests, unittest.TestCase): diff --git a/importlib_resources/tests/test_reader.py b/importlib_resources/tests/test_reader.py index 4370b850..e2fad42e 100644 --- a/importlib_resources/tests/test_reader.py +++ b/importlib_resources/tests/test_reader.py @@ -5,8 +5,6 @@ from importlib import import_module from importlib_resources.readers import MultiplexedPath, NamespaceReader -from .._compat import FileNotFoundError, NotADirectoryError - class MultiplexedPathTest(unittest.TestCase): @classmethod diff --git a/importlib_resources/tests/test_resource.py b/importlib_resources/tests/test_resource.py index 2151a1ad..4dbbb84e 100644 --- a/importlib_resources/tests/test_resource.py +++ b/importlib_resources/tests/test_resource.py @@ -3,8 +3,7 @@ import unittest import importlib_resources as resources import uuid - -from importlib_resources._compat import Path +from pathlib import Path from . import data01 from . import zipdata01, zipdata02 diff --git a/importlib_resources/tests/util.py b/importlib_resources/tests/util.py index 1d6fa729..b8a69078 100644 --- a/importlib_resources/tests/util.py +++ b/importlib_resources/tests/util.py @@ -4,10 +4,10 @@ import sys import types import unittest +from pathlib import Path, PurePath from . import data01 from . import zipdata01 -from .._compat import ABC, Path, PurePath, FileNotFoundError from ..abc import ResourceReader from ._compat import import_helper @@ -76,7 +76,7 @@ def contents(self): return module -class CommonTests(ABC): +class CommonTests(metaclass=abc.ABCMeta): @abc.abstractmethod def execute(self, package, path): diff --git a/setup.cfg b/setup.cfg index 916040f9..8d7cae21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,21 +11,17 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Topic :: Software Development :: Libraries - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: only project_urls = Documentation = https://importlib-resources.readthedocs.io/ [options] packages = find: include_package_data = true -python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.* +python_requires = >=3.6 install_requires = - pathlib2; python_version < '3' - typing; python_version < '3.5' zipp >= 0.4; python_version < '3.8' - singledispatch; python_version < '3.4' - contextlib2; python_version < '3' setup_requires = setuptools_scm[toml] >= 3.4.1 [mypy] diff --git a/tox.ini b/tox.ini index ca607e64..05947b6f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] # Coverage is missing on later version of Python -envlist = {py27,py36,py37,py38,py39}{,-cov,-diffcov},qa,docs +envlist = {py36,py37,py38,py39}{,-cov,-diffcov},qa,docs skip_missing_interpreters = True toxworkdir={env:TOX_WORK_DIR:.tox} @@ -29,8 +29,6 @@ setenv = cov: COVERAGE_PROCESS_START={[coverage]rcfile} cov: COVERAGE_OPTIONS="-p" cov: COVERAGE_FILE={toxinidir}/.coverage - py27: OMIT=3 - py36,py37,py38,py39: OMIT=2 [testenv:qa]