Merge pull request #5063 from asottile/importlib_metadata_v2

Switch to importlib-metadata
This commit is contained in:
Anthony Sottile 2019-05-27 15:00:12 -07:00 committed by GitHub
commit 0a57124063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 185 deletions

View File

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

View File

@ -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",
]

View File

@ -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.
"""

View File

@ -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'"
% (

View File

@ -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),

View File

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

View File

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

View File

@ -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

View File

@ -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