Skip to content

Commit ac4a855

Browse files
committed
fix: replace pkg_resources with importlib.resources/metadata for setuptools 82 compat
1 parent 7e0472b commit ac4a855

7 files changed

Lines changed: 117 additions & 36 deletions

File tree

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def find_version(*file_paths):
5757
"cfn_flip>=1.2.3",
5858
"nested-lookup",
5959
"botocore>=1.31.17",
60+
"importlib_resources>=5.0;python_version<'3.9'",
6061
],
6162
entry_points={
6263
"console_scripts": ["cfn-cli = rpdk.core.cli:main", "cfn = rpdk.core.cli:main"]

src/rpdk/core/data_loaders.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from io import TextIOWrapper
77
from pathlib import Path
88

9-
import pkg_resources
9+
try:
10+
from importlib.resources import files as importlib_resources_files
11+
except ImportError: # Python < 3.9: importlib.resources.files() added in 3.9
12+
from importlib_resources import files as importlib_resources_files
1013
import referencing
1114
import referencing.exceptions
1215
import yaml
@@ -38,7 +41,9 @@ def resource_stream(package_name, resource_name, encoding="utf-8"):
3841
Decoding errors raise :exc:`ValueError`. :term:`universal newlines`
3942
are enabled. Can be used in a ``with`` statement.
4043
"""
41-
f = pkg_resources.resource_stream(package_name, resource_name)
44+
import sys
45+
pkg = sys.modules[package_name].__spec__.parent or package_name
46+
f = importlib_resources_files(pkg).joinpath(resource_name).open("rb")
4247
return TextIOWrapper(f, encoding=encoding)
4348

4449

@@ -55,8 +60,10 @@ def resource_yaml(package_name, resource_name):
5560

5661

5762
def copy_resource(package_name, resource_name, out_path):
58-
with pkg_resources.resource_stream(
59-
package_name, resource_name
63+
import sys
64+
pkg = sys.modules[package_name].__spec__.parent or package_name
65+
with importlib_resources_files(pkg).joinpath(resource_name).open(
66+
"rb"
6067
) as fsrc, out_path.open("wb") as fdst:
6168
shutil.copyfileobj(fsrc, fdst)
6269

src/rpdk/core/plugin_registry.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
1-
import pkg_resources
1+
try:
2+
from importlib.metadata import entry_points as importlib_entry_points
3+
except ImportError: # Python < 3.8
4+
from importlib_metadata import entry_points as importlib_entry_points
5+
6+
7+
def _iter_entry_points(group):
8+
eps = importlib_entry_points()
9+
if hasattr(eps, "select"): # Python 3.12+
10+
return eps.select(group=group)
11+
return eps.get(group, [])
12+
213

314
PLUGIN_REGISTRY = {
415
entry_point.name: entry_point.load
5-
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
16+
for entry_point in _iter_entry_points("rpdk.v1.languages")
617
}
718

819

920
def get_plugin_choices():
10-
plugin_choices = [
11-
entry_point.name
12-
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.languages")
13-
]
14-
return sorted(set(plugin_choices))
21+
return sorted({ep.name for ep in _iter_entry_points("rpdk.v1.languages")})
1522

1623

1724
def get_parsers():
18-
parsers = {
19-
entry_point.name: entry_point.load
20-
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.parsers")
21-
}
22-
23-
return parsers
25+
return {ep.name: ep.load for ep in _iter_entry_points("rpdk.v1.parsers")}
2426

2527

2628
def get_extensions():
27-
extensions = {
28-
entry_point.name: entry_point.load
29-
for entry_point in pkg_resources.iter_entry_points("rpdk.v1.extensions")
30-
}
31-
32-
return extensions
29+
return {ep.name: ep.load for ep in _iter_entry_points("rpdk.v1.extensions")}
3330

3431

3532
def load_plugin(language):
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Integration test: validates that data_loaders and plugin_registry work correctly
3+
without pkg_resources, using only importlib.resources / importlib.metadata.
4+
5+
Run with:
6+
pytest tests/functional_importlib_compat.py -v
7+
"""
8+
import sys
9+
import importlib
10+
import pytest
11+
12+
13+
def test_no_pkg_resources_imported_by_data_loaders():
14+
"""data_loaders must not import pkg_resources at all."""
15+
# Force reimport to catch top-level imports
16+
if "rpdk.core.data_loaders" in sys.modules:
17+
del sys.modules["rpdk.core.data_loaders"]
18+
19+
import unittest.mock as mock
20+
with mock.patch.dict("sys.modules", {"pkg_resources": None}):
21+
# Should not raise ModuleNotFoundError
22+
import rpdk.core.data_loaders # noqa: F401
23+
24+
25+
def test_no_pkg_resources_imported_by_plugin_registry():
26+
"""plugin_registry must not import pkg_resources at all."""
27+
if "rpdk.core.plugin_registry" in sys.modules:
28+
del sys.modules["rpdk.core.plugin_registry"]
29+
30+
import unittest.mock as mock
31+
with mock.patch.dict("sys.modules", {"pkg_resources": None}):
32+
import rpdk.core.plugin_registry # noqa: F401
33+
34+
35+
def test_resource_json_loads_real_schema():
36+
"""resource_json must load an actual bundled schema file end-to-end."""
37+
from rpdk.core.data_loaders import resource_json
38+
schema = resource_json(
39+
"rpdk.core", "data/schema/provider.definition.schema.v1.json"
40+
)
41+
assert "$schema" in schema or "properties" in schema
42+
43+
44+
def test_resource_stream_returns_readable_content():
45+
"""resource_stream must return a readable text stream for a bundled file."""
46+
from rpdk.core.data_loaders import resource_stream
47+
with resource_stream(
48+
"rpdk.core", "data/schema/provider.definition.schema.v1.json"
49+
) as f:
50+
content = f.read()
51+
assert len(content) > 0
52+
assert "$schema" in content or "properties" in content
53+
54+
55+
def test_plugin_registry_get_plugin_choices_does_not_raise():
56+
"""get_plugin_choices must not raise even with no plugins installed."""
57+
from rpdk.core.plugin_registry import get_plugin_choices
58+
choices = get_plugin_choices()
59+
assert isinstance(choices, list)
60+
61+
62+
def test_importlib_resources_files_available():
63+
"""Verify the compat shim resolves correctly on this Python version."""
64+
if sys.version_info >= (3, 9):
65+
from importlib.resources import files
66+
else:
67+
from importlib_resources import files
68+
assert callable(files)

tests/test_data_loaders.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from io import BytesIO, StringIO
77
from pathlib import Path
88
from subprocess import check_output
9-
from unittest.mock import ANY, create_autospec, patch
9+
from unittest.mock import ANY, Mock, create_autospec, patch
1010

1111
import pytest
1212
import yaml
@@ -620,10 +620,18 @@ def plugin():
620620

621621
def mock_pkg_resource_stream(bytes_in, func=resource_stream):
622622
resource_name = "data/test.utf-8"
623-
target = "rpdk.core.data_loaders.pkg_resources.resource_stream"
624-
with patch(target, autospec=True, return_value=BytesIO(bytes_in)) as mock_stream:
623+
target = "rpdk.core.data_loaders.importlib_resources_files"
624+
mock_open = Mock(return_value=BytesIO(bytes_in))
625+
mock_path = Mock()
626+
mock_path.open = mock_open
627+
mock_joinpath = Mock(return_value=mock_path)
628+
mock_files = Mock()
629+
mock_files.joinpath = mock_joinpath
630+
with patch(target, return_value=mock_files) as mock_stream:
625631
f = func(__name__, resource_name)
626-
mock_stream.assert_called_once_with(__name__, resource_name)
632+
mock_stream.assert_called_once_with("tests")
633+
mock_joinpath.assert_called_once_with(resource_name)
634+
mock_open.assert_called_once_with("rb")
627635
return f
628636

629637

tests/test_plugin_registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_get_extensions():
1818
mock_entrypoint_2 = Mock()
1919

2020
patch_iter_entry_points = patch(
21-
"rpdk.core.plugin_registry.pkg_resources.iter_entry_points"
21+
"rpdk.core.plugin_registry._iter_entry_points"
2222
)
2323
with patch_iter_entry_points as mock_iter_entry_points:
2424
mock_iter_entry_points.return_value = [mock_entrypoint_1, mock_entrypoint_2]

tests/utils.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from random import sample
66
from unittest.mock import Mock, patch
77

8-
import pkg_resources
8+
import importlib.metadata
9+
from unittest.mock import patch as _patch
910

1011
from rpdk.core.project import Project
1112

@@ -74,14 +75,13 @@ def chdir(path):
7475

7576

7677
def add_dummy_language_plugin():
77-
distribution = pkg_resources.Distribution(__file__)
78-
entry_point = pkg_resources.EntryPoint.parse(
79-
"dummy = rpdk.dummy:DummyLanguagePlugin", dist=distribution
78+
ep = importlib.metadata.EntryPoint(
79+
name="dummy", value="rpdk.dummy:DummyLanguagePlugin", group="rpdk.v1.languages"
8080
)
81-
distribution._ep_map = { # pylint: disable=protected-access
82-
"rpdk.v1.languages": {"dummy": entry_point}
83-
}
84-
pkg_resources.working_set.add(distribution)
81+
_patch(
82+
"rpdk.core.plugin_registry._iter_entry_points",
83+
side_effect=lambda group: [ep] if group == "rpdk.v1.languages" else [],
84+
).start()
8585

8686

8787
def get_mock_project():

0 commit comments

Comments
 (0)