Skip to content

Commit 0a57124

Browse files
authored
Merge pull request #5063 from asottile/importlib_metadata_v2
Switch to importlib-metadata
2 parents b3f8fab + 13f02af commit 0a57124

File tree

9 files changed

+95
-185
lines changed

9 files changed

+95
-185
lines changed

changelog/5063.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.

setup.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
INSTALL_REQUIRES = [
66
"py>=1.5.0",
77
"six>=1.10.0",
8-
"setuptools",
8+
"packaging",
99
"attrs>=17.4.0",
1010
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
1111
'more-itertools>=4.0.0;python_version>"2.7"',
1212
"atomicwrites>=1.0",
1313
'funcsigs>=1.0;python_version<"3.0"',
1414
'pathlib2>=2.2.0;python_version<"3.6"',
1515
'colorama;sys_platform=="win32"',
16-
"pluggy>=0.9,!=0.10,<1.0",
16+
"pluggy>=0.12,<1.0",
17+
"importlib-metadata>=0.12",
1718
"wcwidth",
1819
]
1920

src/_pytest/assertion/rewrite.py

-19
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def __init__(self, config):
6464
self.session = None
6565
self.modules = {}
6666
self._rewritten_names = set()
67-
self._register_with_pkg_resources()
6867
self._must_rewrite = set()
6968
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
7069
# which might result in infinite recursion (#3506)
@@ -315,24 +314,6 @@ def is_package(self, name):
315314
tp = desc[2]
316315
return tp == imp.PKG_DIRECTORY
317316

318-
@classmethod
319-
def _register_with_pkg_resources(cls):
320-
"""
321-
Ensure package resources can be loaded from this loader. May be called
322-
multiple times, as the operation is idempotent.
323-
"""
324-
try:
325-
import pkg_resources
326-
327-
# access an attribute in case a deferred importer is present
328-
pkg_resources.__name__
329-
except ImportError:
330-
return
331-
332-
# Since pytest tests are always located in the file system, the
333-
# DefaultProvider is appropriate.
334-
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
335-
336317
def get_data(self, pathname):
337318
"""Optional PEP302 get_data API.
338319
"""

src/_pytest/config/__init__.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import types
1313
import warnings
1414

15+
import importlib_metadata
1516
import py
1617
import six
18+
from packaging.version import Version
1719
from pluggy import HookimplMarker
1820
from pluggy import HookspecMarker
1921
from pluggy import PluginManager
@@ -787,25 +789,17 @@ def _mark_plugins_for_rewrite(self, hook):
787789
modules or packages in the distribution package for
788790
all pytest plugins.
789791
"""
790-
import pkg_resources
791-
792792
self.pluginmanager.rewrite_hook = hook
793793

794794
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
795795
# We don't autoload from setuptools entry points, no need to continue.
796796
return
797797

798-
# 'RECORD' available for plugins installed normally (pip install)
799-
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
800-
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
801-
# so it shouldn't be an issue
802-
metadata_files = "RECORD", "SOURCES.txt"
803-
804798
package_files = (
805-
entry.split(",")[0]
806-
for entrypoint in pkg_resources.iter_entry_points("pytest11")
807-
for metadata in metadata_files
808-
for entry in entrypoint.dist._get_metadata(metadata)
799+
str(file)
800+
for dist in importlib_metadata.distributions()
801+
if any(ep.group == "pytest11" for ep in dist.entry_points)
802+
for file in dist.files
809803
)
810804

811805
for name in _iter_rewritable_modules(package_files):
@@ -874,11 +868,10 @@ def _preparse(self, args, addopts=True):
874868

875869
def _checkversion(self):
876870
import pytest
877-
from pkg_resources import parse_version
878871

879872
minver = self.inicfg.get("minversion", None)
880873
if minver:
881-
if parse_version(minver) > parse_version(pytest.__version__):
874+
if Version(minver) > Version(pytest.__version__):
882875
raise pytest.UsageError(
883876
"%s:%d: requires pytest-%s, actual pytest-%s'"
884877
% (

src/_pytest/outcomes.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import sys
1010

11+
from packaging.version import Version
12+
1113

1214
class OutcomeException(BaseException):
1315
""" OutcomeException and its subclass instances indicate and
@@ -175,15 +177,7 @@ def importorskip(modname, minversion=None, reason=None):
175177
return mod
176178
verattr = getattr(mod, "__version__", None)
177179
if minversion is not None:
178-
try:
179-
from pkg_resources import parse_version as pv
180-
except ImportError:
181-
raise Skipped(
182-
"we have a required version for %r but can not import "
183-
"pkg_resources to parse version strings." % (modname,),
184-
allow_module_level=True,
185-
)
186-
if verattr is None or pv(verattr) < pv(minversion):
180+
if verattr is None or Version(verattr) < Version(minversion):
187181
raise Skipped(
188182
"module %r has __version__ %r, required is: %r"
189183
% (modname, verattr, minversion),

testing/acceptance_test.py

+10-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import types
1010

1111
import attr
12+
import importlib_metadata
1213
import py
1314
import six
1415

@@ -111,8 +112,6 @@ def test_option(pytestconfig):
111112

112113
@pytest.mark.parametrize("load_cov_early", [True, False])
113114
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
114-
pkg_resources = pytest.importorskip("pkg_resources")
115-
116115
testdir.makepyfile(mytestplugin1_module="")
117116
testdir.makepyfile(mytestplugin2_module="")
118117
testdir.makepyfile(mycov_module="")
@@ -124,38 +123,28 @@ def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
124123
class DummyEntryPoint(object):
125124
name = attr.ib()
126125
module = attr.ib()
127-
version = "1.0"
128-
129-
@property
130-
def project_name(self):
131-
return self.name
126+
group = "pytest11"
132127

133128
def load(self):
134129
__import__(self.module)
135130
loaded.append(self.name)
136131
return sys.modules[self.module]
137132

138-
@property
139-
def dist(self):
140-
return self
141-
142-
def _get_metadata(self, *args):
143-
return []
144-
145133
entry_points = [
146134
DummyEntryPoint("myplugin1", "mytestplugin1_module"),
147135
DummyEntryPoint("myplugin2", "mytestplugin2_module"),
148136
DummyEntryPoint("mycov", "mycov_module"),
149137
]
150138

151-
def my_iter(group, name=None):
152-
assert group == "pytest11"
153-
for ep in entry_points:
154-
if name is not None and ep.name != name:
155-
continue
156-
yield ep
139+
@attr.s
140+
class DummyDist(object):
141+
entry_points = attr.ib()
142+
files = ()
143+
144+
def my_dists():
145+
return (DummyDist(entry_points),)
157146

158-
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
147+
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
159148
params = ("-p", "mycov") if load_cov_early else ()
160149
testdir.runpytest_inprocess(*params)
161150
if load_cov_early:

testing/test_assertion.py

+20-36
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ def test_foo(pytestconfig):
137137
def test_pytest_plugins_rewrite_module_names_correctly(self, testdir):
138138
"""Test that we match files correctly when they are marked for rewriting (#2939)."""
139139
contents = {
140-
"conftest.py": """
140+
"conftest.py": """\
141141
pytest_plugins = "ham"
142142
""",
143143
"ham.py": "",
144144
"hamster.py": "",
145-
"test_foo.py": """
145+
"test_foo.py": """\
146146
def test_foo(pytestconfig):
147147
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
148148
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
@@ -153,14 +153,13 @@ def test_foo(pytestconfig):
153153
assert result.ret == 0
154154

155155
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
156-
@pytest.mark.parametrize("plugin_state", ["development", "installed"])
157-
def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch):
156+
def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch):
158157
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
159158
# Make sure the hook is installed early enough so that plugins
160159
# installed via setuptools are rewritten.
161160
testdir.tmpdir.join("hampkg").ensure(dir=1)
162161
contents = {
163-
"hampkg/__init__.py": """
162+
"hampkg/__init__.py": """\
164163
import pytest
165164
166165
@pytest.fixture
@@ -169,7 +168,7 @@ def check(values, value):
169168
assert values.pop(0) == value
170169
return check
171170
""",
172-
"spamplugin.py": """
171+
"spamplugin.py": """\
173172
import pytest
174173
from hampkg import check_first2
175174
@@ -179,46 +178,31 @@ def check(values, value):
179178
assert values.pop(0) == value
180179
return check
181180
""",
182-
"mainwrapper.py": """
183-
import pytest, pkg_resources
184-
185-
plugin_state = "{plugin_state}"
186-
187-
class DummyDistInfo(object):
188-
project_name = 'spam'
189-
version = '1.0'
190-
191-
def _get_metadata(self, name):
192-
# 'RECORD' meta-data only available in installed plugins
193-
if name == 'RECORD' and plugin_state == "installed":
194-
return ['spamplugin.py,sha256=abc,123',
195-
'hampkg/__init__.py,sha256=abc,123']
196-
# 'SOURCES.txt' meta-data only available for plugins in development mode
197-
elif name == 'SOURCES.txt' and plugin_state == "development":
198-
return ['spamplugin.py',
199-
'hampkg/__init__.py']
200-
return []
181+
"mainwrapper.py": """\
182+
import pytest, importlib_metadata
201183
202184
class DummyEntryPoint(object):
203185
name = 'spam'
204186
module_name = 'spam.py'
205-
attrs = ()
206-
extras = None
207-
dist = DummyDistInfo()
187+
group = 'pytest11'
208188
209-
def load(self, require=True, *args, **kwargs):
189+
def load(self):
210190
import spamplugin
211191
return spamplugin
212192
213-
def iter_entry_points(group, name=None):
214-
yield DummyEntryPoint()
193+
class DummyDistInfo(object):
194+
version = '1.0'
195+
files = ('spamplugin.py', 'hampkg/__init__.py')
196+
entry_points = (DummyEntryPoint(),)
197+
metadata = {'name': 'foo'}
215198
216-
pkg_resources.iter_entry_points = iter_entry_points
199+
def distributions():
200+
return (DummyDistInfo(),)
201+
202+
importlib_metadata.distributions = distributions
217203
pytest.main()
218-
""".format(
219-
plugin_state=plugin_state
220-
),
221-
"test_foo.py": """
204+
""",
205+
"test_foo.py": """\
222206
def test(check_first):
223207
check_first([10, 30], 30)
224208

0 commit comments

Comments
 (0)