Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to importlib-metadata #5063

Merged
merged 1 commit into from
May 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/5063.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
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"',
"atomicwrites>=1.0",
'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",
]

Expand Down
19 changes: 0 additions & 19 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def __init__(self, config):
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)
Expand Down Expand Up @@ -315,24 +314,6 @@ def is_package(self, name):
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.
"""
Expand Down
21 changes: 7 additions & 14 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
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
Expand Down Expand Up @@ -787,25 +789,17 @@ def _mark_plugins_for_rewrite(self, hook):
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):
Expand Down Expand Up @@ -874,11 +868,10 @@ def _preparse(self, args, addopts=True):

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'"
% (
Expand Down
12 changes: 3 additions & 9 deletions src/_pytest/outcomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import sys

from packaging.version import Version


class OutcomeException(BaseException):
""" OutcomeException and its subclass instances indicate and
Expand Down Expand Up @@ -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),
Expand Down
31 changes: 10 additions & 21 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import types

import attr
import importlib_metadata
import py
import six

Expand Down Expand Up @@ -111,8 +112,6 @@ def test_option(pytestconfig):

@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="")
Expand All @@ -124,38 +123,28 @@ def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
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 = ()

def my_dists():
return (DummyDist(entry_points),)

monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
monkeypatch.setattr(importlib_metadata, "distributions", my_dists)
params = ("-p", "mycov") if load_cov_early else ()
testdir.runpytest_inprocess(*params)
if load_cov_early:
Expand Down
56 changes: 20 additions & 36 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ def test_foo(pytestconfig):
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": """\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the escaped newline required for some reason?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no but it makes the line numbers add up and makes my editor less angry about tqs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tqs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

triple quoted strings

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
Expand All @@ -153,14 +153,13 @@ def test_foo(pytestconfig):
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
Expand All @@ -169,7 +168,7 @@ def check(values, value):
assert values.pop(0) == value
return check
""",
"spamplugin.py": """
"spamplugin.py": """\
import pytest
from hampkg import check_first2

Expand All @@ -179,46 +178,31 @@ def check(values, value):
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)

Expand Down
Loading