Skip to content

Commit

Permalink
Add package scoped fixtures #2283
Browse files Browse the repository at this point in the history
  • Loading branch information
turturica authored and turturica committed Apr 11, 2018
1 parent 372bcdb commit 2b14108
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 15 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Hugo van Kemenade
Hui Wang (coldnight)
Ian Bicking
Ian Lesperance
Ionuț Turturică
Jaap Broekhuizen
Jan Balster
Janne Vanhala
Expand Down
10 changes: 7 additions & 3 deletions _pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def pytest_sessionstart(session):
import _pytest.nodes

scopename2class.update({
'package': _pytest.python.Package,
'class': _pytest.python.Class,
'module': _pytest.python.Module,
'function': _pytest.nodes.Item,
Expand All @@ -48,6 +49,7 @@ def pytest_sessionstart(session):


scope2props = dict(session=())
scope2props["package"] = ("fspath",)
scope2props["module"] = ("fspath", "module")
scope2props["class"] = scope2props["module"] + ("cls",)
scope2props["instance"] = scope2props["class"] + ("instance", )
Expand Down Expand Up @@ -156,9 +158,11 @@ def get_parametrized_fixture_keys(item, scopenum):
continue
if scopenum == 0: # session
key = (argname, param_index)
elif scopenum == 1: # module
elif scopenum == 1: # package
key = (argname, param_index, item.fspath)
elif scopenum == 2: # class
elif scopenum == 2: # module
key = (argname, param_index, item.fspath)
elif scopenum == 3: # class
key = (argname, param_index, item.fspath, item.cls)
yield key

Expand Down Expand Up @@ -596,7 +600,7 @@ class ScopeMismatchError(Exception):
"""


scopes = "session module class function".split()
scopes = "session package module class function".split()
scopenum_function = scopes.index("function")


Expand Down
31 changes: 22 additions & 9 deletions _pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,17 +405,30 @@ def collect(self):

def _collect(self, arg):
names = self._parsearg(arg)
path = names.pop(0)
if path.check(dir=1):
argpath = names.pop(0)
paths = []
if argpath.check(dir=1):
assert not names, "invalid arg %r" % (arg,)
for path in path.visit(fil=lambda x: x.check(file=1),
rec=self._recurse, bf=True, sort=True):
for x in self._collectfile(path):
yield x
for path in argpath.visit(fil=lambda x: x.check(file=1),
rec=self._recurse, bf=True, sort=True):
pkginit = path.dirpath().join('__init__.py')
if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
for x in self._collectfile(pkginit):
yield x
paths.append(x.fspath.dirpath())

if not any(x in path.parts() for x in paths):
for x in self._collectfile(path):
yield x
else:
assert path.check(file=1)
for x in self.matchnodes(self._collectfile(path), names):
yield x
assert argpath.check(file=1)
pkginit = argpath.dirpath().join('__init__.py')
if not self.isinitpath(argpath) and pkginit.exists():
for x in self._collectfile(pkginit):
yield x
else:
for x in self.matchnodes(self._collectfile(argpath), names):
yield x

def _collectfile(self, path):
ihook = self.gethookproxy(path)
Expand Down
47 changes: 46 additions & 1 deletion _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from _pytest.config import hookimpl

import _pytest
from _pytest.main import Session
import pluggy
from _pytest import fixtures
from _pytest import nodes
Expand Down Expand Up @@ -157,7 +158,7 @@ def pytest_collect_file(path, parent):
ext = path.ext
if ext == ".py":
if not parent.session.isinitpath(path):
for pat in parent.config.getini('python_files'):
for pat in parent.config.getini('python_files') + ['__init__.py']:
if path.fnmatch(pat):
break
else:
Expand All @@ -167,9 +168,23 @@ def pytest_collect_file(path, parent):


def pytest_pycollect_makemodule(path, parent):
if path.basename == '__init__.py':
return Package(path, parent)
return Module(path, parent)


def pytest_ignore_collect(path, config):
# Skip duplicate packages.
keepduplicates = config.getoption("keepduplicates")
if keepduplicates:
duplicate_paths = config.pluginmanager._duplicatepaths
if path.basename == '__init__.py':
if path in duplicate_paths:
return True
else:
duplicate_paths.add(path)


@hookimpl(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield
Expand Down Expand Up @@ -475,6 +490,36 @@ def setup(self):
self.addfinalizer(teardown_module)


class Package(Session, Module):

def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
session = parent.session
nodes.FSCollector.__init__(
self, fspath, parent=parent,
config=config, session=session, nodeid=nodeid)
self.name = fspath.pyimport().__name__
self.trace = session.trace
self._norecursepatterns = session._norecursepatterns
for path in list(session.config.pluginmanager._duplicatepaths):
if path.dirname == fspath.dirname and path != fspath:
session.config.pluginmanager._duplicatepaths.remove(path)
pass

def isinitpath(self, path):
return path in self.session._initialpaths

def collect(self):
path = self.fspath.dirpath()
pkg_prefix = None
for path in path.visit(fil=lambda x: 1,
rec=self._recurse, bf=True, sort=True):
if pkg_prefix and pkg_prefix in path.parts():
continue
for x in self._collectfile(path):
yield x
if isinstance(x, Package):
pkg_prefix = path.dirpath()

def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
"""
Return a callable to perform xunit-style setup or teardown if
Expand Down
1 change: 1 addition & 0 deletions changelog/2283.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pytest now supports package-level fixtures.
33 changes: 33 additions & 0 deletions testing/python/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,39 @@ def test_x(one):
reprec = testdir.inline_run("..")
reprec.assertoutcome(passed=2)

def test_package_xunit_fixture(self, testdir):
testdir.makepyfile(__init__="""\
values = []
""")
package = testdir.mkdir("package")
package.join("__init__.py").write(dedent("""\
from .. import values
def setup_module():
values.append("package")
def teardown_module():
values[:] = []
"""))
package.join("test_x.py").write(dedent("""\
from .. import values
def test_x():
assert values == ["package"]
"""))
package = testdir.mkdir("package2")
package.join("__init__.py").write(dedent("""\
from .. import values
def setup_module():
values.append("package2")
def teardown_module():
values[:] = []
"""))
package.join("test_x.py").write(dedent("""\
from .. import values
def test_x():
assert values == ["package2"]
"""))
reprec = testdir.inline_run()
reprec.assertoutcome(passed=2)


class TestAutouseDiscovery(object):

Expand Down
2 changes: 1 addition & 1 deletion testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ def test_continue_on_collection_errors_maxfail(testdir):

def test_fixture_scope_sibling_conftests(testdir):
"""Regression test case for https://github.com/pytest-dev/pytest/issues/2836"""
foo_path = testdir.mkpydir("foo")
foo_path = testdir.mkdir("foo")
foo_path.join("conftest.py").write(_pytest._code.Source("""
import pytest
@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion testing/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class TestY(TestX):
started = reprec.getcalls("pytest_collectstart")
finished = reprec.getreports("pytest_collectreport")
assert len(started) == len(finished)
assert len(started) == 7 # XXX extra TopCollector
assert len(started) == 8 # XXX extra TopCollector
colfail = [x for x in finished if x.failed]
assert len(colfail) == 1

Expand Down

0 comments on commit 2b14108

Please sign in to comment.