Skip to content

Commit 698c385

Browse files
authored
replace pkg_resources with importlib.resources/metadata for setuptools 82 compat (#1118)
* fix: replace pkg_resources with importlib.resources/metadata for setuptools 82 compat * fix: update known_third_party in setup.cfg for importlib migration * fix: apply pre-commit linting fixes (isort, black, unused imports) * fix: move sys import to top-level, drop patch alias in utils * fix: use rpdk.core package in resource_json call for importlib.resources compat
1 parent 7e0472b commit 698c385

9 files changed

Lines changed: 122 additions & 42 deletions

File tree

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ include_trailing_comma = true
2929
combine_as_imports = True
3030
force_grid_wrap = 0
3131
known_first_party = rpdk
32-
known_third_party = boto3,botocore,cfn_tools,cfnlint,colorama,docker,hypothesis,jinja2,jsonpatch,jsonschema,nested_lookup,ordered_set,pkg_resources,pytest,pytest_localserver,referencing,requests,setuptools,yaml
32+
known_third_party = boto3,botocore,cfn_tools,cfnlint,colorama,docker,hypothesis,jinja2,jsonpatch,jsonschema,nested_lookup,ordered_set,pytest,pytest_localserver,referencing,requests,setuptools,yaml
3333

3434
[tool:pytest]
3535
# can't do anything about 3rd part modules, so don't spam us

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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import os
44
import re
55
import shutil
6+
import sys
67
from io import TextIOWrapper
78
from pathlib import Path
89

9-
import pkg_resources
10+
try:
11+
from importlib.resources import files as importlib_resources_files
12+
except ImportError: # Python < 3.9: importlib.resources.files() added in 3.9
13+
from importlib_resources import files as importlib_resources_files
1014
import referencing
1115
import referencing.exceptions
1216
import yaml
@@ -38,7 +42,8 @@ def resource_stream(package_name, resource_name, encoding="utf-8"):
3842
Decoding errors raise :exc:`ValueError`. :term:`universal newlines`
3943
are enabled. Can be used in a ``with`` statement.
4044
"""
41-
f = pkg_resources.resource_stream(package_name, resource_name)
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,9 @@ 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+
pkg = sys.modules[package_name].__spec__.parent or package_name
64+
with importlib_resources_files(pkg).joinpath(resource_name).open(
65+
"rb"
6066
) as fsrc, out_path.open("wb") as fdst:
6167
shutil.copyfileobj(fsrc, fdst)
6268

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):

tests/fragments/test_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def test_overwrite_doesnt_exist(template_fragment, tmpdir):
373373

374374
def __make_resource_validator(base_uri=None, timeout=TIMEOUT_IN_SECONDS):
375375
schema = resource_json(
376-
__name__,
377-
"../../src/rpdk/core/data/schema/provider.definition.schema.modules.v1.json",
376+
"rpdk.core",
377+
"data/schema/provider.definition.schema.modules.v1.json",
378378
)
379379
return make_validator(schema)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
# pylint: disable=import-outside-toplevel
9+
import sys
10+
11+
12+
def test_no_pkg_resources_imported_by_data_loaders():
13+
"""data_loaders must not import pkg_resources at all."""
14+
from unittest import mock
15+
16+
# Force reimport to catch top-level imports
17+
if "rpdk.core.data_loaders" in sys.modules:
18+
del sys.modules["rpdk.core.data_loaders"]
19+
20+
with mock.patch.dict("sys.modules", {"pkg_resources": None}):
21+
# Should not raise ModuleNotFoundError
22+
__import__("rpdk.core.data_loaders")
23+
24+
25+
def test_no_pkg_resources_imported_by_plugin_registry():
26+
"""plugin_registry must not import pkg_resources at all."""
27+
from unittest import mock
28+
29+
if "rpdk.core.plugin_registry" in sys.modules:
30+
del sys.modules["rpdk.core.plugin_registry"]
31+
32+
with mock.patch.dict("sys.modules", {"pkg_resources": None}):
33+
__import__("rpdk.core.plugin_registry")
34+
35+
36+
def test_resource_json_loads_real_schema():
37+
"""resource_json must load an actual bundled schema file end-to-end."""
38+
from rpdk.core.data_loaders import resource_json
39+
40+
schema = resource_json(
41+
"rpdk.core", "data/schema/provider.definition.schema.v1.json"
42+
)
43+
assert "$schema" in schema or "properties" in schema
44+
45+
46+
def test_resource_stream_returns_readable_content():
47+
"""resource_stream must return a readable text stream for a bundled file."""
48+
from rpdk.core.data_loaders import resource_stream
49+
50+
with resource_stream(
51+
"rpdk.core", "data/schema/provider.definition.schema.v1.json"
52+
) as f:
53+
content = f.read()
54+
assert len(content) > 0
55+
assert "$schema" in content or "properties" in content
56+
57+
58+
def test_plugin_registry_get_plugin_choices_does_not_raise():
59+
"""get_plugin_choices must not raise even with no plugins installed."""
60+
from rpdk.core.plugin_registry import get_plugin_choices
61+
62+
choices = get_plugin_choices()
63+
assert isinstance(choices, list)
64+
65+
66+
def test_importlib_resources_files_available():
67+
"""Verify the compat shim resolves correctly on this Python version."""
68+
if sys.version_info >= (3, 9):
69+
from importlib.resources import files
70+
else:
71+
from importlib_resources import files
72+
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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ def test_get_extensions():
1717
mock_entrypoint_1 = Mock()
1818
mock_entrypoint_2 = Mock()
1919

20-
patch_iter_entry_points = patch(
21-
"rpdk.core.plugin_registry.pkg_resources.iter_entry_points"
22-
)
20+
patch_iter_entry_points = patch("rpdk.core.plugin_registry._iter_entry_points")
2321
with patch_iter_entry_points as mock_iter_entry_points:
2422
mock_iter_entry_points.return_value = [mock_entrypoint_1, mock_entrypoint_2]
2523

tests/utils.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import importlib.metadata
12
import os
23
from contextlib import contextmanager
34
from io import BytesIO
45
from pathlib import Path
56
from random import sample
67
from unittest.mock import Mock, patch
78

8-
import pkg_resources
9-
109
from rpdk.core.project import Project
1110

1211
CONTENTS_UTF8 = "💣"
@@ -74,14 +73,13 @@ def chdir(path):
7473

7574

7675
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
76+
ep = importlib.metadata.EntryPoint(
77+
name="dummy", value="rpdk.dummy:DummyLanguagePlugin", group="rpdk.v1.languages"
8078
)
81-
distribution._ep_map = { # pylint: disable=protected-access
82-
"rpdk.v1.languages": {"dummy": entry_point}
83-
}
84-
pkg_resources.working_set.add(distribution)
79+
patch(
80+
"rpdk.core.plugin_registry._iter_entry_points",
81+
side_effect=lambda group: [ep] if group == "rpdk.v1.languages" else [],
82+
).start()
8583

8684

8785
def get_mock_project():

0 commit comments

Comments
 (0)