Merge pull request #5063 from asottile/importlib_metadata_v2
Switch to importlib-metadata
This commit is contained in:
commit
0a57124063
|
@ -0,0 +1 @@
|
|||
Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
|
5
setup.py
5
setup.py
|
@ -5,7 +5,7 @@ from setuptools import setup
|
|||
INSTALL_REQUIRES = [
|
||||
"py>=1.5.0",
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"packaging",
|
||||
"attrs>=17.4.0",
|
||||
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
|
||||
'more-itertools>=4.0.0;python_version>"2.7"',
|
||||
|
@ -13,7 +13,8 @@ INSTALL_REQUIRES = [
|
|||
'funcsigs>=1.0;python_version<"3.0"',
|
||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||
'colorama;sys_platform=="win32"',
|
||||
"pluggy>=0.9,!=0.10,<1.0",
|
||||
"pluggy>=0.12,<1.0",
|
||||
"importlib-metadata>=0.12",
|
||||
"wcwidth",
|
||||
]
|
||||
|
||||
|
|
|
@ -64,7 +64,6 @@ class AssertionRewritingHook(object):
|
|||
self.session = None
|
||||
self.modules = {}
|
||||
self._rewritten_names = set()
|
||||
self._register_with_pkg_resources()
|
||||
self._must_rewrite = set()
|
||||
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
||||
# which might result in infinite recursion (#3506)
|
||||
|
@ -315,24 +314,6 @@ class AssertionRewritingHook(object):
|
|||
tp = desc[2]
|
||||
return tp == imp.PKG_DIRECTORY
|
||||
|
||||
@classmethod
|
||||
def _register_with_pkg_resources(cls):
|
||||
"""
|
||||
Ensure package resources can be loaded from this loader. May be called
|
||||
multiple times, as the operation is idempotent.
|
||||
"""
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
# access an attribute in case a deferred importer is present
|
||||
pkg_resources.__name__
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
# Since pytest tests are always located in the file system, the
|
||||
# DefaultProvider is appropriate.
|
||||
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
|
||||
|
||||
def get_data(self, pathname):
|
||||
"""Optional PEP302 get_data API.
|
||||
"""
|
||||
|
|
|
@ -12,8 +12,10 @@ import sys
|
|||
import types
|
||||
import warnings
|
||||
|
||||
import importlib_metadata
|
||||
import py
|
||||
import six
|
||||
from packaging.version import Version
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import PluginManager
|
||||
|
@ -787,25 +789,17 @@ class Config(object):
|
|||
modules or packages in the distribution package for
|
||||
all pytest plugins.
|
||||
"""
|
||||
import pkg_resources
|
||||
|
||||
self.pluginmanager.rewrite_hook = hook
|
||||
|
||||
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# We don't autoload from setuptools entry points, no need to continue.
|
||||
return
|
||||
|
||||
# 'RECORD' available for plugins installed normally (pip install)
|
||||
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
|
||||
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
|
||||
# so it shouldn't be an issue
|
||||
metadata_files = "RECORD", "SOURCES.txt"
|
||||
|
||||
package_files = (
|
||||
entry.split(",")[0]
|
||||
for entrypoint in pkg_resources.iter_entry_points("pytest11")
|
||||
for metadata in metadata_files
|
||||
for entry in entrypoint.dist._get_metadata(metadata)
|
||||
str(file)
|
||||
for dist in importlib_metadata.distributions()
|
||||
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
||||
for file in dist.files
|
||||
)
|
||||
|
||||
for name in _iter_rewritable_modules(package_files):
|
||||
|
@ -874,11 +868,10 @@ class Config(object):
|
|||
|
||||
def _checkversion(self):
|
||||
import pytest
|
||||
from pkg_resources import parse_version
|
||||
|
||||
minver = self.inicfg.get("minversion", None)
|
||||
if minver:
|
||||
if parse_version(minver) > parse_version(pytest.__version__):
|
||||
if Version(minver) > Version(pytest.__version__):
|
||||
raise pytest.UsageError(
|
||||
"%s:%d: requires pytest-%s, actual pytest-%s'"
|
||||
% (
|
||||
|
|
|
@ -8,6 +8,8 @@ from __future__ import print_function
|
|||
|
||||
import sys
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
|
||||
class OutcomeException(BaseException):
|
||||
""" OutcomeException and its subclass instances indicate and
|
||||
|
@ -175,15 +177,7 @@ def importorskip(modname, minversion=None, reason=None):
|
|||
return mod
|
||||
verattr = getattr(mod, "__version__", None)
|
||||
if minversion is not None:
|
||||
try:
|
||||
from pkg_resources import parse_version as pv
|
||||
except ImportError:
|
||||
raise Skipped(
|
||||
"we have a required version for %r but can not import "
|
||||
"pkg_resources to parse version strings." % (modname,),
|
||||
allow_module_level=True,
|
||||
)
|
||||
if verattr is None or pv(verattr) < pv(minversion):
|
||||
if verattr is None or Version(verattr) < Version(minversion):
|
||||
raise Skipped(
|
||||
"module %r has __version__ %r, required is: %r"
|
||||
% (modname, verattr, minversion),
|
||||
|
|
|
@ -9,6 +9,7 @@ import textwrap
|
|||
import types
|
||||
|
||||
import attr
|
||||
import importlib_metadata
|
||||
import py
|
||||
import six
|
||||
|
||||
|
@ -111,8 +112,6 @@ class TestGeneralUsage(object):
|
|||
|
||||
@pytest.mark.parametrize("load_cov_early", [True, False])
|
||||
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
|
||||
pkg_resources = pytest.importorskip("pkg_resources")
|
||||
|
||||
testdir.makepyfile(mytestplugin1_module="")
|
||||
testdir.makepyfile(mytestplugin2_module="")
|
||||
testdir.makepyfile(mycov_module="")
|
||||
|
@ -124,38 +123,28 @@ class TestGeneralUsage(object):
|
|||
class DummyEntryPoint(object):
|
||||
name = attr.ib()
|
||||
module = attr.ib()
|
||||
version = "1.0"
|
||||
|
||||
@property
|
||||
def project_name(self):
|
||||
return self.name
|
||||
group = "pytest11"
|
||||
|
||||
def load(self):
|
||||
__import__(self.module)
|
||||
loaded.append(self.name)
|
||||
return sys.modules[self.module]
|
||||
|
||||
@property
|
||||
def dist(self):
|
||||
return self
|
||||
|
||||
def _get_metadata(self, *args):
|
||||
return []
|
||||
|
||||
entry_points = [
|
||||
DummyEntryPoint("myplugin1", "mytestplugin1_module"),
|
||||
DummyEntryPoint("myplugin2", "mytestplugin2_module"),
|
||||
DummyEntryPoint("mycov", "mycov_module"),
|
||||
]
|
||||
|
||||
def my_iter(group, name=None):
|
||||
assert group == "pytest11"
|
||||
for ep in entry_points:
|
||||
if name is not None and ep.name != name:
|
||||
continue
|
||||
yield ep
|
||||
@attr.s
|
||||
class DummyDist(object):
|
||||
entry_points = attr.ib()
|
||||
files = ()
|
||||
|
||||
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
|
||||
def my_dists():
|
||||
return (DummyDist(entry_points),)
|
||||
|
||||
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
|
||||
params = ("-p", "mycov") if load_cov_early else ()
|
||||
testdir.runpytest_inprocess(*params)
|
||||
if load_cov_early:
|
||||
|
|
|
@ -137,12 +137,12 @@ class TestImportHookInstallation(object):
|
|||
def test_pytest_plugins_rewrite_module_names_correctly(self, testdir):
|
||||
"""Test that we match files correctly when they are marked for rewriting (#2939)."""
|
||||
contents = {
|
||||
"conftest.py": """
|
||||
"conftest.py": """\
|
||||
pytest_plugins = "ham"
|
||||
""",
|
||||
"ham.py": "",
|
||||
"hamster.py": "",
|
||||
"test_foo.py": """
|
||||
"test_foo.py": """\
|
||||
def test_foo(pytestconfig):
|
||||
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
|
||||
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
|
||||
|
@ -153,14 +153,13 @@ class TestImportHookInstallation(object):
|
|||
assert result.ret == 0
|
||||
|
||||
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
|
||||
@pytest.mark.parametrize("plugin_state", ["development", "installed"])
|
||||
def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch):
|
||||
def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch):
|
||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||
# Make sure the hook is installed early enough so that plugins
|
||||
# installed via setuptools are rewritten.
|
||||
testdir.tmpdir.join("hampkg").ensure(dir=1)
|
||||
contents = {
|
||||
"hampkg/__init__.py": """
|
||||
"hampkg/__init__.py": """\
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -169,7 +168,7 @@ class TestImportHookInstallation(object):
|
|||
assert values.pop(0) == value
|
||||
return check
|
||||
""",
|
||||
"spamplugin.py": """
|
||||
"spamplugin.py": """\
|
||||
import pytest
|
||||
from hampkg import check_first2
|
||||
|
||||
|
@ -179,46 +178,31 @@ class TestImportHookInstallation(object):
|
|||
assert values.pop(0) == value
|
||||
return check
|
||||
""",
|
||||
"mainwrapper.py": """
|
||||
import pytest, pkg_resources
|
||||
|
||||
plugin_state = "{plugin_state}"
|
||||
|
||||
class DummyDistInfo(object):
|
||||
project_name = 'spam'
|
||||
version = '1.0'
|
||||
|
||||
def _get_metadata(self, name):
|
||||
# 'RECORD' meta-data only available in installed plugins
|
||||
if name == 'RECORD' and plugin_state == "installed":
|
||||
return ['spamplugin.py,sha256=abc,123',
|
||||
'hampkg/__init__.py,sha256=abc,123']
|
||||
# 'SOURCES.txt' meta-data only available for plugins in development mode
|
||||
elif name == 'SOURCES.txt' and plugin_state == "development":
|
||||
return ['spamplugin.py',
|
||||
'hampkg/__init__.py']
|
||||
return []
|
||||
"mainwrapper.py": """\
|
||||
import pytest, importlib_metadata
|
||||
|
||||
class DummyEntryPoint(object):
|
||||
name = 'spam'
|
||||
module_name = 'spam.py'
|
||||
attrs = ()
|
||||
extras = None
|
||||
dist = DummyDistInfo()
|
||||
group = 'pytest11'
|
||||
|
||||
def load(self, require=True, *args, **kwargs):
|
||||
def load(self):
|
||||
import spamplugin
|
||||
return spamplugin
|
||||
|
||||
def iter_entry_points(group, name=None):
|
||||
yield DummyEntryPoint()
|
||||
class DummyDistInfo(object):
|
||||
version = '1.0'
|
||||
files = ('spamplugin.py', 'hampkg/__init__.py')
|
||||
entry_points = (DummyEntryPoint(),)
|
||||
metadata = {'name': 'foo'}
|
||||
|
||||
pkg_resources.iter_entry_points = iter_entry_points
|
||||
def distributions():
|
||||
return (DummyDistInfo(),)
|
||||
|
||||
importlib_metadata.distributions = distributions
|
||||
pytest.main()
|
||||
""".format(
|
||||
plugin_state=plugin_state
|
||||
),
|
||||
"test_foo.py": """
|
||||
""",
|
||||
"test_foo.py": """\
|
||||
def test(check_first):
|
||||
check_first([10, 30], 30)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import print_function
|
|||
import sys
|
||||
import textwrap
|
||||
|
||||
import attr
|
||||
import importlib_metadata
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
|
@ -531,22 +531,11 @@ def test_options_on_small_file_do_not_blow_up(testdir):
|
|||
|
||||
|
||||
def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
|
||||
pkg_resources = pytest.importorskip("pkg_resources")
|
||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||
|
||||
def my_iter(group, name=None):
|
||||
assert group == "pytest11"
|
||||
|
||||
class Dist(object):
|
||||
project_name = "spam"
|
||||
version = "1.0"
|
||||
|
||||
def _get_metadata(self, name):
|
||||
return ["foo.txt,sha256=abc,123"]
|
||||
|
||||
class EntryPoint(object):
|
||||
name = "mytestplugin"
|
||||
dist = Dist()
|
||||
group = "pytest11"
|
||||
|
||||
def load(self):
|
||||
class PseudoPlugin(object):
|
||||
|
@ -554,9 +543,14 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
|
|||
|
||||
return PseudoPlugin()
|
||||
|
||||
return iter([EntryPoint()])
|
||||
class Dist(object):
|
||||
files = ()
|
||||
entry_points = (EntryPoint(),)
|
||||
|
||||
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
|
||||
def my_dists():
|
||||
return (Dist,)
|
||||
|
||||
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
pytest_plugins = "mytestplugin",
|
||||
|
@ -569,60 +563,50 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
|
|||
|
||||
|
||||
def test_setuptools_importerror_issue1479(testdir, monkeypatch):
|
||||
pkg_resources = pytest.importorskip("pkg_resources")
|
||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||
|
||||
def my_iter(group, name=None):
|
||||
assert group == "pytest11"
|
||||
|
||||
class Dist(object):
|
||||
project_name = "spam"
|
||||
version = "1.0"
|
||||
|
||||
def _get_metadata(self, name):
|
||||
return ["foo.txt,sha256=abc,123"]
|
||||
|
||||
class EntryPoint(object):
|
||||
class DummyEntryPoint(object):
|
||||
name = "mytestplugin"
|
||||
dist = Dist()
|
||||
group = "pytest11"
|
||||
|
||||
def load(self):
|
||||
raise ImportError("Don't hide me!")
|
||||
|
||||
return iter([EntryPoint()])
|
||||
class Distribution(object):
|
||||
version = "1.0"
|
||||
files = ("foo.txt",)
|
||||
entry_points = (DummyEntryPoint(),)
|
||||
|
||||
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
|
||||
def distributions():
|
||||
return (Distribution(),)
|
||||
|
||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
||||
with pytest.raises(ImportError):
|
||||
testdir.parseconfig()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block_it", [True, False])
|
||||
def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it):
|
||||
pkg_resources = pytest.importorskip("pkg_resources")
|
||||
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
|
||||
|
||||
plugin_module_placeholder = object()
|
||||
|
||||
def my_iter(group, name=None):
|
||||
assert group == "pytest11"
|
||||
|
||||
class Dist(object):
|
||||
project_name = "spam"
|
||||
version = "1.0"
|
||||
|
||||
def _get_metadata(self, name):
|
||||
return ["foo.txt,sha256=abc,123"]
|
||||
|
||||
class EntryPoint(object):
|
||||
class DummyEntryPoint(object):
|
||||
name = "mytestplugin"
|
||||
dist = Dist()
|
||||
group = "pytest11"
|
||||
|
||||
def load(self):
|
||||
return plugin_module_placeholder
|
||||
|
||||
return iter([EntryPoint()])
|
||||
class Distribution(object):
|
||||
version = "1.0"
|
||||
files = ("foo.txt",)
|
||||
entry_points = (DummyEntryPoint(),)
|
||||
|
||||
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
|
||||
def distributions():
|
||||
return (Distribution(),)
|
||||
|
||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
||||
args = ("-p", "no:mytestplugin") if block_it else ()
|
||||
config = testdir.parseconfig(*args)
|
||||
config.pluginmanager.import_plugin("mytestplugin")
|
||||
|
@ -639,37 +623,26 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block
|
|||
"parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
|
||||
)
|
||||
def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load):
|
||||
pkg_resources = pytest.importorskip("pkg_resources")
|
||||
|
||||
def my_iter(group, name=None):
|
||||
assert group == "pytest11"
|
||||
assert name == "mytestplugin"
|
||||
return iter([DummyEntryPoint()])
|
||||
|
||||
@attr.s
|
||||
class DummyEntryPoint(object):
|
||||
name = "mytestplugin"
|
||||
project_name = name = "mytestplugin"
|
||||
group = "pytest11"
|
||||
version = "1.0"
|
||||
|
||||
@property
|
||||
def project_name(self):
|
||||
return self.name
|
||||
|
||||
def load(self):
|
||||
return sys.modules[self.name]
|
||||
|
||||
@property
|
||||
def dist(self):
|
||||
return self
|
||||
|
||||
def _get_metadata(self, *args):
|
||||
return []
|
||||
class Distribution(object):
|
||||
entry_points = (DummyEntryPoint(),)
|
||||
files = ()
|
||||
|
||||
class PseudoPlugin(object):
|
||||
x = 42
|
||||
|
||||
def distributions():
|
||||
return (Distribution(),)
|
||||
|
||||
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
|
||||
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
|
||||
monkeypatch.setattr(importlib_metadata, "distributions", distributions)
|
||||
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin())
|
||||
config = testdir.parseconfig(*parse_args)
|
||||
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
|
||||
|
|
|
@ -2,16 +2,10 @@ from __future__ import absolute_import
|
|||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entrypoint", ["py.test", "pytest"])
|
||||
def test_entry_point_exist(entrypoint):
|
||||
assert entrypoint in pkg_resources.get_entry_map("pytest")["console_scripts"]
|
||||
import importlib_metadata
|
||||
|
||||
|
||||
def test_pytest_entry_points_are_identical():
|
||||
entryMap = pkg_resources.get_entry_map("pytest")["console_scripts"]
|
||||
assert entryMap["pytest"].module_name == entryMap["py.test"].module_name
|
||||
dist = importlib_metadata.distribution("pytest")
|
||||
entry_map = {ep.name: ep for ep in dist.entry_points}
|
||||
assert entry_map["pytest"].value == entry_map["py.test"].value
|
||||
|
|
Loading…
Reference in New Issue