Skip to content

Commit 5067f08

Browse files
Remove dependency on pkg_resources from setuptools (#1536)
Avoid using `importlib.util.find_spec()` to avoid actually importing parent packages.
1 parent a0cc074 commit 5067f08

File tree

6 files changed

+55
-26
lines changed

6 files changed

+55
-26
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Release date: TBA
1515

1616
Closes #1512
1717

18+
* Remove dependency on ``pkg_resources`` from ``setuptools``.
19+
20+
Closes #1103
21+
1822
* Allowed ``AstroidManager.clear_cache`` to reload necessary brain plugins.
1923

2024
* Fixed incorrect inferences after rebuilding the builtins module, e.g. by calling

astroid/interpreter/_import/spec.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import importlib.machinery
1111
import importlib.util
1212
import os
13+
import pathlib
1314
import sys
1415
import zipimport
1516
from collections.abc import Sequence
@@ -147,7 +148,7 @@ def contribute_to_path(self, spec, processed):
147148
# Builtin.
148149
return None
149150

150-
if _is_setuptools_namespace(spec.location):
151+
if _is_setuptools_namespace(Path(spec.location)):
151152
# extend_path is called, search sys.path for module/packages
152153
# of this name see pkgutil.extend_path documentation
153154
path = [
@@ -179,7 +180,7 @@ def contribute_to_path(self, spec, processed):
179180

180181

181182
class ExplicitNamespacePackageFinder(ImportlibFinder):
182-
"""A finder for the explicit namespace packages, generated through pkg_resources."""
183+
"""A finder for the explicit namespace packages."""
183184

184185
def find_module(self, modname, module_parts, processed, submodule_path):
185186
if processed:
@@ -253,12 +254,12 @@ def contribute_to_path(self, spec, processed):
253254
)
254255

255256

256-
def _is_setuptools_namespace(location):
257+
def _is_setuptools_namespace(location: pathlib.Path) -> bool:
257258
try:
258-
with open(os.path.join(location, "__init__.py"), "rb") as stream:
259+
with open(location / "__init__.py", "rb") as stream:
259260
data = stream.read(4096)
260261
except OSError:
261-
return None
262+
return False
262263
else:
263264
extend_path = b"pkgutil" in data and b"extend_path" in data
264265
declare_namespace = (

astroid/interpreter/_import/util.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,35 @@
22
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
33
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
44

5-
try:
6-
import pkg_resources
7-
except ImportError:
8-
pkg_resources = None # type: ignore[assignment]
5+
import sys
6+
from functools import lru_cache
7+
from importlib.util import _find_spec_from_path
98

109

11-
def is_namespace(modname):
12-
return (
13-
pkg_resources is not None
14-
and hasattr(pkg_resources, "_namespace_packages")
15-
and modname in pkg_resources._namespace_packages
16-
)
10+
@lru_cache(maxsize=4096)
11+
def is_namespace(modname: str) -> bool:
12+
if modname in sys.builtin_module_names:
13+
return False
14+
15+
found_spec = None
16+
17+
# find_spec() attempts to import parent packages when given dotted paths.
18+
# That's unacceptable here, so we fallback to _find_spec_from_path(), which does
19+
# not, but requires instead that each single parent ('astroid', 'nodes', etc.)
20+
# be specced from left to right.
21+
processed_components = []
22+
last_parent = None
23+
for component in modname.split("."):
24+
processed_components.append(component)
25+
working_modname = ".".join(processed_components)
26+
try:
27+
found_spec = _find_spec_from_path(working_modname, last_parent)
28+
except ValueError:
29+
# executed .pth files may not have __spec__
30+
return True
31+
last_parent = working_modname
32+
33+
if found_spec is None:
34+
return False
35+
36+
return found_spec.origin is None

astroid/manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from astroid.const import BRAIN_MODULES_DIRECTORY
2020
from astroid.exceptions import AstroidBuildingError, AstroidImportError
21-
from astroid.interpreter._import import spec
21+
from astroid.interpreter._import import spec, util
2222
from astroid.modutils import (
2323
NoSourceFile,
2424
_cache_normalize_path_,
@@ -384,6 +384,7 @@ def clear_cache(self) -> None:
384384
for lru_cache in (
385385
LookupMixIn.lookup,
386386
_cache_normalize_path_,
387+
util.is_namespace,
387388
ObjectModel.attributes,
388389
):
389390
lru_cache.cache_clear()

setup.cfg

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ packages = find:
3939
install_requires =
4040
lazy_object_proxy>=1.4.0
4141
wrapt>=1.11,<2
42-
setuptools>=20.0
4342
typed-ast>=1.4.0,<2.0;implementation_name=="cpython" and python_version<"3.8"
4443
typing-extensions>=3.10;python_version<"3.10"
4544
python_requires = >=3.7.2

tests/unittest_manager.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
from collections.abc import Iterator
1111
from contextlib import contextmanager
1212

13-
import pkg_resources
14-
1513
import astroid
1614
from astroid import manager, test_utils
1715
from astroid.const import IS_JYTHON
1816
from astroid.exceptions import AstroidBuildingError, AstroidImportError
17+
from astroid.interpreter._import import util
1918
from astroid.modutils import is_standard_module
2019
from astroid.nodes import Const
2120
from astroid.nodes.scoped_nodes import ClassDef
@@ -111,6 +110,16 @@ def test_ast_from_namespace_pkgutil(self) -> None:
111110
def test_ast_from_namespace_pkg_resources(self) -> None:
112111
self._test_ast_from_old_namespace_package_protocol("pkg_resources")
113112

113+
def test_identify_old_namespace_package_protocol(self) -> None:
114+
# Like the above cases, this package follows the old namespace package protocol
115+
# astroid currently assumes such packages are in sys.modules, so import it
116+
# pylint: disable-next=import-outside-toplevel
117+
import tests.testdata.python3.data.path_pkg_resources_1.package.foo as _ # noqa
118+
119+
self.assertTrue(
120+
util.is_namespace("tests.testdata.python3.data.path_pkg_resources_1")
121+
)
122+
114123
def test_implicit_namespace_package(self) -> None:
115124
data_dir = os.path.dirname(resources.find("data/namespace_pep_420"))
116125
contribute = os.path.join(data_dir, "contribute_to_namespace")
@@ -131,7 +140,6 @@ def test_implicit_namespace_package(self) -> None:
131140
def test_namespace_package_pth_support(self) -> None:
132141
pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
133142
site.addpackage(resources.RESOURCE_PATH, pth, [])
134-
pkg_resources._namespace_packages["foogle"] = []
135143

136144
try:
137145
module = self.manager.ast_from_module_name("foogle.fax")
@@ -141,18 +149,14 @@ def test_namespace_package_pth_support(self) -> None:
141149
with self.assertRaises(AstroidImportError):
142150
self.manager.ast_from_module_name("foogle.moogle")
143151
finally:
144-
del pkg_resources._namespace_packages["foogle"]
145152
sys.modules.pop("foogle")
146153

147154
def test_nested_namespace_import(self) -> None:
148155
pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
149156
site.addpackage(resources.RESOURCE_PATH, pth, [])
150-
pkg_resources._namespace_packages["foogle"] = ["foogle.crank"]
151-
pkg_resources._namespace_packages["foogle.crank"] = []
152157
try:
153158
self.manager.ast_from_module_name("foogle.crank")
154159
finally:
155-
del pkg_resources._namespace_packages["foogle"]
156160
sys.modules.pop("foogle")
157161

158162
def test_namespace_and_file_mismatch(self) -> None:
@@ -161,12 +165,10 @@ def test_namespace_and_file_mismatch(self) -> None:
161165
self.assertEqual(ast.name, "unittest")
162166
pth = "foogle_fax-0.12.5-py2.7-nspkg.pth"
163167
site.addpackage(resources.RESOURCE_PATH, pth, [])
164-
pkg_resources._namespace_packages["foogle"] = []
165168
try:
166169
with self.assertRaises(AstroidImportError):
167170
self.manager.ast_from_module_name("unittest.foogle.fax")
168171
finally:
169-
del pkg_resources._namespace_packages["foogle"]
170172
sys.modules.pop("foogle")
171173

172174
def _test_ast_from_zip(self, archive: str) -> None:
@@ -323,6 +325,7 @@ def test_clear_cache_clears_other_lru_caches(self) -> None:
323325
lrus = (
324326
astroid.nodes.node_classes.LookupMixIn.lookup,
325327
astroid.modutils._cache_normalize_path_,
328+
util.is_namespace,
326329
astroid.interpreter.objectmodel.ObjectModel.attributes,
327330
)
328331

@@ -332,6 +335,7 @@ def test_clear_cache_clears_other_lru_caches(self) -> None:
332335
# Generate some hits and misses
333336
ClassDef().lookup("garbage")
334337
is_standard_module("unittest", std_path=["garbage_path"])
338+
util.is_namespace("unittest")
335339
astroid.interpreter.objectmodel.ObjectModel().attributes()
336340

337341
# Did the hits or misses actually happen?

0 commit comments

Comments
 (0)