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 = [ INSTALL_REQUIRES = [
"py>=1.5.0", "py>=1.5.0",
"six>=1.10.0", "six>=1.10.0",
"setuptools", "packaging",
"attrs>=17.4.0", "attrs>=17.4.0",
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"', 'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
'more-itertools>=4.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"', 'funcsigs>=1.0;python_version<"3.0"',
'pathlib2>=2.2.0;python_version<"3.6"', 'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"', 'colorama;sys_platform=="win32"',
"pluggy>=0.9,!=0.10,<1.0", "pluggy>=0.12,<1.0",
"importlib-metadata>=0.12",
"wcwidth", "wcwidth",
] ]

View File

@ -64,7 +64,6 @@ class AssertionRewritingHook(object):
self.session = None self.session = None
self.modules = {} self.modules = {}
self._rewritten_names = set() self._rewritten_names = set()
self._register_with_pkg_resources()
self._must_rewrite = set() self._must_rewrite = set()
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # 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) # which might result in infinite recursion (#3506)
@ -315,24 +314,6 @@ class AssertionRewritingHook(object):
tp = desc[2] tp = desc[2]
return tp == imp.PKG_DIRECTORY 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): def get_data(self, pathname):
"""Optional PEP302 get_data API. """Optional PEP302 get_data API.
""" """

View File

@ -12,8 +12,10 @@ import sys
import types import types
import warnings import warnings
import importlib_metadata
import py import py
import six import six
from packaging.version import Version
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import PluginManager from pluggy import PluginManager
@ -787,25 +789,17 @@ class Config(object):
modules or packages in the distribution package for modules or packages in the distribution package for
all pytest plugins. all pytest plugins.
""" """
import pkg_resources
self.pluginmanager.rewrite_hook = hook self.pluginmanager.rewrite_hook = hook
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# We don't autoload from setuptools entry points, no need to continue. # We don't autoload from setuptools entry points, no need to continue.
return 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 = ( package_files = (
entry.split(",")[0] str(file)
for entrypoint in pkg_resources.iter_entry_points("pytest11") for dist in importlib_metadata.distributions()
for metadata in metadata_files if any(ep.group == "pytest11" for ep in dist.entry_points)
for entry in entrypoint.dist._get_metadata(metadata) for file in dist.files
) )
for name in _iter_rewritable_modules(package_files): for name in _iter_rewritable_modules(package_files):
@ -874,11 +868,10 @@ class Config(object):
def _checkversion(self): def _checkversion(self):
import pytest import pytest
from pkg_resources import parse_version
minver = self.inicfg.get("minversion", None) minver = self.inicfg.get("minversion", None)
if minver: if minver:
if parse_version(minver) > parse_version(pytest.__version__): if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError( raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'" "%s:%d: requires pytest-%s, actual pytest-%s'"
% ( % (

View File

@ -8,6 +8,8 @@ from __future__ import print_function
import sys import sys
from packaging.version import Version
class OutcomeException(BaseException): class OutcomeException(BaseException):
""" OutcomeException and its subclass instances indicate and """ OutcomeException and its subclass instances indicate and
@ -175,15 +177,7 @@ def importorskip(modname, minversion=None, reason=None):
return mod return mod
verattr = getattr(mod, "__version__", None) verattr = getattr(mod, "__version__", None)
if minversion is not None: if minversion is not None:
try: if verattr is None or Version(verattr) < Version(minversion):
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):
raise Skipped( raise Skipped(
"module %r has __version__ %r, required is: %r" "module %r has __version__ %r, required is: %r"
% (modname, verattr, minversion), % (modname, verattr, minversion),

View File

@ -9,6 +9,7 @@ import textwrap
import types import types
import attr import attr
import importlib_metadata
import py import py
import six import six
@ -111,8 +112,6 @@ class TestGeneralUsage(object):
@pytest.mark.parametrize("load_cov_early", [True, False]) @pytest.mark.parametrize("load_cov_early", [True, False])
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
pkg_resources = pytest.importorskip("pkg_resources")
testdir.makepyfile(mytestplugin1_module="") testdir.makepyfile(mytestplugin1_module="")
testdir.makepyfile(mytestplugin2_module="") testdir.makepyfile(mytestplugin2_module="")
testdir.makepyfile(mycov_module="") testdir.makepyfile(mycov_module="")
@ -124,38 +123,28 @@ class TestGeneralUsage(object):
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = attr.ib() name = attr.ib()
module = attr.ib() module = attr.ib()
version = "1.0" group = "pytest11"
@property
def project_name(self):
return self.name
def load(self): def load(self):
__import__(self.module) __import__(self.module)
loaded.append(self.name) loaded.append(self.name)
return sys.modules[self.module] return sys.modules[self.module]
@property
def dist(self):
return self
def _get_metadata(self, *args):
return []
entry_points = [ entry_points = [
DummyEntryPoint("myplugin1", "mytestplugin1_module"), DummyEntryPoint("myplugin1", "mytestplugin1_module"),
DummyEntryPoint("myplugin2", "mytestplugin2_module"), DummyEntryPoint("myplugin2", "mytestplugin2_module"),
DummyEntryPoint("mycov", "mycov_module"), DummyEntryPoint("mycov", "mycov_module"),
] ]
def my_iter(group, name=None): @attr.s
assert group == "pytest11" class DummyDist(object):
for ep in entry_points: entry_points = attr.ib()
if name is not None and ep.name != name: files = ()
continue
yield ep
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 () params = ("-p", "mycov") if load_cov_early else ()
testdir.runpytest_inprocess(*params) testdir.runpytest_inprocess(*params)
if load_cov_early: if load_cov_early:

View File

@ -137,12 +137,12 @@ class TestImportHookInstallation(object):
def test_pytest_plugins_rewrite_module_names_correctly(self, testdir): def test_pytest_plugins_rewrite_module_names_correctly(self, testdir):
"""Test that we match files correctly when they are marked for rewriting (#2939).""" """Test that we match files correctly when they are marked for rewriting (#2939)."""
contents = { contents = {
"conftest.py": """ "conftest.py": """\
pytest_plugins = "ham" pytest_plugins = "ham"
""", """,
"ham.py": "", "ham.py": "",
"hamster.py": "", "hamster.py": "",
"test_foo.py": """ "test_foo.py": """\
def test_foo(pytestconfig): def test_foo(pytestconfig):
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
@ -153,14 +153,13 @@ class TestImportHookInstallation(object):
assert result.ret == 0 assert result.ret == 0
@pytest.mark.parametrize("mode", ["plain", "rewrite"]) @pytest.mark.parametrize("mode", ["plain", "rewrite"])
@pytest.mark.parametrize("plugin_state", ["development", "installed"]) def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch):
def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch):
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
# Make sure the hook is installed early enough so that plugins # Make sure the hook is installed early enough so that plugins
# installed via setuptools are rewritten. # installed via setuptools are rewritten.
testdir.tmpdir.join("hampkg").ensure(dir=1) testdir.tmpdir.join("hampkg").ensure(dir=1)
contents = { contents = {
"hampkg/__init__.py": """ "hampkg/__init__.py": """\
import pytest import pytest
@pytest.fixture @pytest.fixture
@ -169,7 +168,7 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value assert values.pop(0) == value
return check return check
""", """,
"spamplugin.py": """ "spamplugin.py": """\
import pytest import pytest
from hampkg import check_first2 from hampkg import check_first2
@ -179,46 +178,31 @@ class TestImportHookInstallation(object):
assert values.pop(0) == value assert values.pop(0) == value
return check return check
""", """,
"mainwrapper.py": """ "mainwrapper.py": """\
import pytest, pkg_resources import pytest, importlib_metadata
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 []
class DummyEntryPoint(object): class DummyEntryPoint(object):
name = 'spam' name = 'spam'
module_name = 'spam.py' module_name = 'spam.py'
attrs = () group = 'pytest11'
extras = None
dist = DummyDistInfo()
def load(self, require=True, *args, **kwargs): def load(self):
import spamplugin import spamplugin
return spamplugin return spamplugin
def iter_entry_points(group, name=None): class DummyDistInfo(object):
yield DummyEntryPoint() 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() pytest.main()
""".format( """,
plugin_state=plugin_state "test_foo.py": """\
),
"test_foo.py": """
def test(check_first): def test(check_first):
check_first([10, 30], 30) check_first([10, 30], 30)

View File

@ -5,7 +5,7 @@ from __future__ import print_function
import sys import sys
import textwrap import textwrap
import attr import importlib_metadata
import _pytest._code import _pytest._code
import pytest import pytest
@ -531,32 +531,26 @@ def test_options_on_small_file_do_not_blow_up(testdir):
def test_preparse_ordering_with_setuptools(testdir, monkeypatch): def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None): class EntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" class PseudoPlugin(object):
version = "1.0" x = 42
def _get_metadata(self, name): return PseudoPlugin()
return ["foo.txt,sha256=abc,123"]
class EntryPoint(object): class Dist(object):
name = "mytestplugin" files = ()
dist = Dist() entry_points = (EntryPoint(),)
def load(self): def my_dists():
class PseudoPlugin(object): return (Dist,)
x = 42
return PseudoPlugin() monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
testdir.makeconftest( testdir.makeconftest(
""" """
pytest_plugins = "mytestplugin", pytest_plugins = "mytestplugin",
@ -569,60 +563,50 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch):
def test_setuptools_importerror_issue1479(testdir, monkeypatch): def test_setuptools_importerror_issue1479(testdir, monkeypatch):
pkg_resources = pytest.importorskip("pkg_resources")
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
def my_iter(group, name=None): class DummyEntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" raise ImportError("Don't hide me!")
version = "1.0"
def _get_metadata(self, name): class Distribution(object):
return ["foo.txt,sha256=abc,123"] version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object): def distributions():
name = "mytestplugin" return (Distribution(),)
dist = Dist()
def load(self): monkeypatch.setattr(importlib_metadata, "distributions", distributions)
raise ImportError("Don't hide me!")
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
with pytest.raises(ImportError): with pytest.raises(ImportError):
testdir.parseconfig() testdir.parseconfig()
@pytest.mark.parametrize("block_it", [True, False]) @pytest.mark.parametrize("block_it", [True, False])
def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): 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) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
plugin_module_placeholder = object() plugin_module_placeholder = object()
def my_iter(group, name=None): class DummyEntryPoint(object):
assert group == "pytest11" name = "mytestplugin"
group = "pytest11"
class Dist(object): def load(self):
project_name = "spam" return plugin_module_placeholder
version = "1.0"
def _get_metadata(self, name): class Distribution(object):
return ["foo.txt,sha256=abc,123"] version = "1.0"
files = ("foo.txt",)
entry_points = (DummyEntryPoint(),)
class EntryPoint(object): def distributions():
name = "mytestplugin" return (Distribution(),)
dist = Dist()
def load(self): monkeypatch.setattr(importlib_metadata, "distributions", distributions)
return plugin_module_placeholder
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
args = ("-p", "no:mytestplugin") if block_it else () args = ("-p", "no:mytestplugin") if block_it else ()
config = testdir.parseconfig(*args) config = testdir.parseconfig(*args)
config.pluginmanager.import_plugin("mytestplugin") 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)] "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
) )
def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): 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): class DummyEntryPoint(object):
name = "mytestplugin" project_name = name = "mytestplugin"
group = "pytest11"
version = "1.0" version = "1.0"
@property
def project_name(self):
return self.name
def load(self): def load(self):
return sys.modules[self.name] return sys.modules[self.name]
@property class Distribution(object):
def dist(self): entry_points = (DummyEntryPoint(),)
return self files = ()
def _get_metadata(self, *args):
return []
class PseudoPlugin(object): class PseudoPlugin(object):
x = 42 x = 42
def distributions():
return (Distribution(),)
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") 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()) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin())
config = testdir.parseconfig(*parse_args) config = testdir.parseconfig(*parse_args)
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None 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 division
from __future__ import print_function from __future__ import print_function
import pkg_resources import importlib_metadata
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"]
def test_pytest_entry_points_are_identical(): def test_pytest_entry_points_are_identical():
entryMap = pkg_resources.get_entry_map("pytest")["console_scripts"] dist = importlib_metadata.distribution("pytest")
assert entryMap["pytest"].module_name == entryMap["py.test"].module_name entry_map = {ep.name: ep for ep in dist.entry_points}
assert entry_map["pytest"].value == entry_map["py.test"].value