From 2aafc66ad82395246c937ac5ca1d5321b4371bad Mon Sep 17 00:00:00 2001 From: Felix Fanghaenel Date: Wed, 11 Feb 2026 13:17:23 +0100 Subject: [PATCH] Combine existing PRs to adapt to modern python > 3.12 --- fs/__init__.py | 2 +- fs/opener/__init__.py | 2 +- fs/opener/registry.py | 37 +++++++++++++++----- fs/test.py | 3 +- setup.cfg | 2 +- tests/test_move.py | 6 ++-- tests/test_opener.py | 81 +++++++++++++++++++++++++++++++------------ 7 files changed, 94 insertions(+), 39 deletions(-) diff --git a/fs/__init__.py b/fs/__init__.py index 97dc55ba..8cddcb13 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__) from . import path from ._fscompat import fsdecode, fsencode diff --git a/fs/opener/__init__.py b/fs/opener/__init__.py index 651a630b..336f7379 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__) # 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..d86acb45 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 @@ -20,6 +20,29 @@ from ..base import FS +if sys.version_info >= (3, 8): + import importlib.metadata + + if sys.version_info >= (3, 10): + + def entrypoints(group, name=None): + ep = importlib.metadata.entry_points(group=group, name=name) + return tuple(n for n in ep) + + else: + + def entrypoints(group, name=None): + ep = importlib.metadata.entry_points() + if name: + return tuple(n for n in ep.get(group, ()) if n.name == name) + return ep.get(group, ()) + +else: + import pkg_resources + + def entrypoints(group, name=None): + return tuple(pkg_resources.iter_entry_points(group, name)) + class Registry(object): """A registry for `Opener` instances.""" @@ -74,10 +97,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(n.name for n in entrypoints("fs.opener")) _protocols = list(collections.OrderedDict.fromkeys(_protocols)) return _protocols @@ -101,10 +121,9 @@ def get_opener(self, protocol): """ protocol = protocol or self.default_opener - if self.load_extern: - entry_point = next( - pkg_resources.iter_entry_points("fs.opener", protocol), None - ) + ep = entrypoints("fs.opener", protocol) + if self.load_extern and ep: + entry_point = ep[0] else: entry_point = None diff --git a/fs/test.py b/fs/test.py index 32e6ea5c..850b3c17 100644 --- a/fs/test.py +++ b/fs/test.py @@ -1082,8 +1082,7 @@ def test_remove(self): self.fs.makedirs("foo/bar/baz/") error_msg = "resource 'foo/bar/egg/test.txt' not found" - assertRaisesRegex = getattr(self, "assertRaisesRegex", self.assertRaisesRegexp) - with assertRaisesRegex(errors.ResourceNotFound, error_msg): + with six.assertRaisesRegex(self, errors.ResourceNotFound, error_msg): self.fs.remove("foo/bar/egg/test.txt") def test_removedir(self): diff --git a/setup.cfg b/setup.cfg index 57c6f40b..fc7182be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ setup_requires = setuptools >=38.3.0 install_requires = appdirs~=1.4.3 - setuptools + setuptools ; python_version < '3.8' six ~=1.10 enum34 ~=1.1.6 ; python_version < '3.4' typing ~=3.6 ; python_version < '3.6' diff --git a/tests/test_move.py b/tests/test_move.py index 8eb1af75..7dd5b114 100644 --- a/tests/test_move.py +++ b/tests/test_move.py @@ -167,7 +167,7 @@ def test_move_file_overwrite(self, _, fs_url): self.assertFalse(src.exists("target.txt")) self.assertFalse(dst.exists("file.txt")) self.assertTrue(dst.exists("target.txt")) - self.assertEquals(dst.readtext("target.txt"), "source content") + self.assertEqual(dst.readtext("target.txt"), "source content") @parameterized.expand([("temp", "temp://"), ("mem", "mem://")]) def test_move_file_overwrite_itself(self, _, fs_url): @@ -177,7 +177,7 @@ def test_move_file_overwrite_itself(self, _, fs_url): tmp.writetext("file.txt", "content") fs.move.move_file(tmp, "file.txt", tmp, "file.txt") self.assertTrue(tmp.exists("file.txt")) - self.assertEquals(tmp.readtext("file.txt"), "content") + self.assertEqual(tmp.readtext("file.txt"), "content") @parameterized.expand([("temp", "temp://"), ("mem", "mem://")]) def test_move_file_overwrite_itself_relpath(self, _, fs_url): @@ -188,7 +188,7 @@ def test_move_file_overwrite_itself_relpath(self, _, fs_url): new_dir.writetext("file.txt", "content") fs.move.move_file(tmp, "dir/../dir/file.txt", tmp, "dir/file.txt") self.assertTrue(tmp.exists("dir/file.txt")) - self.assertEquals(tmp.readtext("dir/file.txt"), "content") + self.assertEqual(tmp.readtext("dir/file.txt"), "content") @parameterized.expand([(True,), (False,)]) def test_move_file_cleanup_on_error(self, cleanup): diff --git a/tests/test_opener.py b/tests/test_opener.py index 43d56903..f8101a3b 100644 --- a/tests/test_opener.py +++ b/tests/test_opener.py @@ -3,7 +3,6 @@ import sys import os -import pkg_resources import shutil import tempfile import unittest @@ -21,6 +20,11 @@ except ImportError: import mock +if sys.version_info >= (3, 8): + import importlib.metadata +else: + import pkg_resources + class TestParse(unittest.TestCase): def test_registry_repr(self): @@ -111,14 +115,25 @@ def test_protocols(self): 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 - ): + if sys.version_info >= (3, 8): + extensions = ( + importlib.metadata.EntryPoint("proto1", "mod1", "fs.opener"), + importlib.metadata.EntryPoint("proto2", "mod2", "fs.opener"), + ) + if sys.version_info >= (3, 10): + m = mock.MagicMock(return_value=extensions) + else: + m = mock.MagicMock(return_value={"fs.opener": extensions}) + patch = mock.patch("importlib.metadata.entry_points", m) + else: + extensions = [ + pkg_resources.EntryPoint("proto1", "mod1"), + pkg_resources.EntryPoint("proto2", "mod2"), + ] + m = mock.MagicMock(return_value=extensions) + patch = mock.patch("pkg_resources.iter_entry_points", m) + + with patch: self.assertIn("proto1", opener.registry.protocols) self.assertIn("proto2", opener.registry.protocols) @@ -129,11 +144,20 @@ def test_unknown_protocol(self): def test_entry_point_load_error(self): entry_point = mock.MagicMock() + entry_point.name = "test" entry_point.load.side_effect = ValueError("some error") - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) - - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + if sys.version_info >= (3, 8): + if sys.version_info >= (3, 10): + entry_points = mock.MagicMock(return_value=tuple([entry_point])) + else: + entry_points = mock.MagicMock(return_value={"fs.opener": [entry_point]}) + patch = mock.patch("importlib.metadata.entry_points", entry_points) + else: + iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + patch = mock.patch("pkg_resources.iter_entry_points", iter_entry_points) + + with patch: with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual( @@ -145,10 +169,19 @@ class NotAnOpener(object): pass entry_point = mock.MagicMock() + entry_point.name = "test" entry_point.load = mock.MagicMock(return_value=NotAnOpener) - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + if sys.version_info >= (3, 8): + if sys.version_info >= (3, 10): + entry_points = mock.MagicMock(return_value=tuple([entry_point])) + else: + entry_points = mock.MagicMock(return_value={"fs.opener": [entry_point]}) + patch = mock.patch("importlib.metadata.entry_points", entry_points) + else: + iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + patch = mock.patch("pkg_resources.iter_entry_points", iter_entry_points) + with patch: with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual("entry point did not return an opener", str(ctx.exception)) @@ -162,10 +195,20 @@ def open_fs(self, *args, **kwargs): pass entry_point = mock.MagicMock() + entry_point.name = "test" entry_point.load = mock.MagicMock(return_value=BadOpener) - iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) - with mock.patch("pkg_resources.iter_entry_points", iter_entry_points): + if sys.version_info >= (3, 8): + if sys.version_info >= (3, 10): + entry_points = mock.MagicMock(return_value=tuple([entry_point])) + else: + entry_points = mock.MagicMock(return_value={"fs.opener": [entry_point]}) + patch = mock.patch("importlib.metadata.entry_points", entry_points) + else: + iter_entry_points = mock.MagicMock(return_value=iter([entry_point])) + patch = mock.patch("pkg_resources.iter_entry_points", iter_entry_points) + + with patch: with self.assertRaises(errors.EntryPointError) as ctx: opener.open_fs("test://") self.assertEqual( @@ -215,12 +258,6 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmpdir) - def test_repr(self): - # Check __repr__ works - for entry_point in pkg_resources.iter_entry_points("fs.opener"): - _opener = entry_point.load() - repr(_opener()) - def test_open_osfs(self): fs = opener.open_fs("osfs://.") self.assertIsInstance(fs, OSFS)