From 7fa40c47573afde7bc23f213341270bc873ebcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Rami=CC=81rez=20Mondrago=CC=81n?= Date: Sat, 24 Sep 2022 01:59:47 -0500 Subject: [PATCH] Drop usage of `pkg_resources` --- fs/__init__.py | 2 +- fs/opener/__init__.py | 2 +- fs/opener/registry.py | 38 ++++++++++++++++++---- setup.cfg | 7 ++-- tests/test_opener.py | 76 +++++++++++++++++++++++++++++-------------- 5 files changed, 88 insertions(+), 37 deletions(-) diff --git a/fs/__init__.py b/fs/__init__.py index 97dc55ba..9b64da66 100644 --- a/fs/__init__.py +++ b/fs/__init__.py @@ -1,7 +1,7 @@ """Python filesystem abstraction layer. """ -__import__("pkg_resources").declare_namespace(__name__) # type: ignore +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore from . import path from ._fscompat import fsdecode, fsencode diff --git a/fs/opener/__init__.py b/fs/opener/__init__.py index 651a630b..60822c4a 100644 --- a/fs/opener/__init__.py +++ b/fs/opener/__init__.py @@ -3,7 +3,7 @@ """ # Declare fs.opener as a namespace package -__import__("pkg_resources").declare_namespace(__name__) # type: ignore +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore # Import opener modules so that `registry.install` if called on each opener from . import appfs, ftpfs, memoryfs, osfs, tarfs, tempfs, zipfs diff --git a/fs/opener/registry.py b/fs/opener/registry.py index 19547234..18ddeb2c 100644 --- a/fs/opener/registry.py +++ b/fs/opener/registry.py @@ -4,11 +4,11 @@ from __future__ import absolute_import, print_function, unicode_literals +import sys import typing import collections import contextlib -import pkg_resources from ..errors import ResourceReadOnly from .base import Opener @@ -16,10 +16,32 @@ from .parse import parse_fs_url if typing.TYPE_CHECKING: - from typing import Callable, Dict, Iterator, List, Text, Tuple, Type, Union + from typing import ( + Callable, + Dict, + Iterator, + List, + Sequence, + Text, + Tuple, + Type, + Union, + ) from ..base import FS +if sys.version_info >= (3, 8): + # The entry_points function takes keyword arguments starting with Python 3.10 + # https://docs.python.org/3.10/library/importlib.metadata.html#entry-points + from importlib import metadata as importlib_metadata +else: + import importlib_metadata + + +def _get_entry_points(): + # type: () -> Sequence[importlib_metadata.EntryPoint] + return importlib_metadata.entry_points().get("fs.opener", []) + class Registry(object): """A registry for `Opener` instances.""" @@ -74,10 +96,7 @@ def protocols(self): """`list`: the list of supported protocols.""" _protocols = list(self._protocols) if self.load_extern: - _protocols.extend( - entry_point.name - for entry_point in pkg_resources.iter_entry_points("fs.opener") - ) + _protocols.extend(entry_point.name for entry_point in _get_entry_points()) _protocols = list(collections.OrderedDict.fromkeys(_protocols)) return _protocols @@ -103,7 +122,12 @@ def get_opener(self, protocol): if self.load_extern: entry_point = next( - pkg_resources.iter_entry_points("fs.opener", protocol), None + ( + entry_point + for entry_point in _get_entry_points() + if entry_point.name == protocol + ), + None, ) else: entry_point = None diff --git a/setup.cfg b/setup.cfg index 57c6f40b..ecabe232 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,9 +45,10 @@ install_requires = appdirs~=1.4.3 setuptools six ~=1.10 - enum34 ~=1.1.6 ; python_version < '3.4' - typing ~=3.6 ; python_version < '3.6' - backports.os ~=0.1 ; python_version < '3.0' + enum34 ~=1.1.6 ; python_version < '3.4' + typing ~=3.6 ; python_version < '3.6' + backports.os ~=0.1 ; python_version < '3.0' + importlib-metadata ~=2.0 ; python_version < '3.8' [options.extras_require] scandir = diff --git a/tests/test_opener.py b/tests/test_opener.py index 43d56903..e8b9222e 100644 --- a/tests/test_opener.py +++ b/tests/test_opener.py @@ -1,9 +1,6 @@ from __future__ import unicode_literals -import sys - import os -import pkg_resources import shutil import tempfile import unittest @@ -13,7 +10,7 @@ from fs.memoryfs import MemoryFS from fs.opener import errors, registry from fs.opener.parse import ParseResult -from fs.opener.registry import Registry +from fs.opener.registry import Registry, importlib_metadata from fs.osfs import OSFS try: @@ -21,6 +18,11 @@ except ImportError: import mock +try: + from pytest import MonkeyPatch +except ImportError: + from _pytest.monkeypatch import MonkeyPatch + class TestParse(unittest.TestCase): def test_registry_repr(self): @@ -106,34 +108,44 @@ def test_parse_params_decode(self): class TestRegistry(unittest.TestCase): + def setUp(self): + self.monkeypatch = MonkeyPatch() + def test_protocols(self): self.assertIsInstance(opener.registry.protocols, list) def test_registry_protocols(self): # Check registry.protocols list the names of all available extension - extensions = [ - pkg_resources.EntryPoint("proto1", "mod1"), - pkg_resources.EntryPoint("proto2", "mod2"), - ] - m = mock.MagicMock(return_value=extensions) - with mock.patch.object( - sys.modules["pkg_resources"], "iter_entry_points", new=m - ): + class EntryPoint(object): + def __init__(self, name): + self.name = name + + def _my_entry_points(): + return {"fs.opener": [EntryPoint("proto1"), EntryPoint("proto2")]} + + with self.monkeypatch.context() as m: + m.setattr(importlib_metadata, "entry_points", _my_entry_points) self.assertIn("proto1", opener.registry.protocols) self.assertIn("proto2", opener.registry.protocols) + self.assertNotIn("wheel", opener.registry.protocols) def test_unknown_protocol(self): with self.assertRaises(errors.UnsupportedProtocol): opener.open_fs("unknown://") def test_entry_point_load_error(self): + class EntryPoint(object): + def __init__(self, name): + self.name = name - entry_point = mock.MagicMock() - entry_point.load.side_effect = ValueError("some error") + def load(self): + raise ValueError("some error") - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + def _my_entry_points(): + return {"fs.opener": [EntryPoint("test")]} - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + with self.monkeypatch.context() as m: + m.setattr(importlib_metadata, "entry_points", _my_entry_points) with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual( @@ -144,11 +156,18 @@ def test_entry_point_type_error(self): class NotAnOpener(object): pass - entry_point = mock.MagicMock() - entry_point.load = mock.MagicMock(return_value=NotAnOpener) - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + class EntryPoint(object): + def __init__(self, name): + self.name = name + + def load(self): + return NotAnOpener - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + def _my_entry_points(): + return {"fs.opener": [EntryPoint("test")]} + + with self.monkeypatch.context() as m: + m.setattr(importlib_metadata, "entry_points", _my_entry_points) with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual("entry point did not return an opener", str(ctx.exception)) @@ -161,11 +180,18 @@ def __init__(self, *args, **kwargs): def open_fs(self, *args, **kwargs): pass - entry_point = mock.MagicMock() - entry_point.load = mock.MagicMock(return_value=BadOpener) - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + class EntryPoint(object): + def __init__(self, name): + self.name = name + + def load(self): + return BadOpener + + def _my_entry_points(): + return {"fs.opener": [EntryPoint("test")]} - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + with self.monkeypatch.context() as m: + m.setattr(importlib_metadata, "entry_points", _my_entry_points) with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual( @@ -217,7 +243,7 @@ def tearDown(self): def test_repr(self): # Check __repr__ works - for entry_point in pkg_resources.iter_entry_points("fs.opener"): + for entry_point in importlib_metadata.entry_points().get("fs.opener", []): _opener = entry_point.load() repr(_opener())