Export types of builtin fixture for type annotations

In order to allow users to type annotate fixtures they request, the
types need to be imported from the `pytest` namespace. They are/were
always available to import from the `_pytest` namespace, but that is
not guaranteed to be stable.

These types are only exported for the purpose of typing. Specifically,
the following are *not* public:

- Construction (`__init__`)
- Subclassing
- staticmethods and classmethods

We try to combat them being used anyway by:

- Marking the classes as `@final` when possible (already done).

- Not documenting private stuff in the API Reference.

- Using `_`-prefixed names or marking as `:meta private:` for private
  stuff.

- Adding a keyword-only `_ispytest=False` to private constructors,
  warning if False, and changing pytest itself to pass True. In the
  future it will (hopefully) become a hard error.

Hopefully that will be enough.
This commit is contained in:
Ran Benita
2020-09-27 22:20:31 +03:00
parent b050578882
commit f1e6fdcddb
19 changed files with 292 additions and 126 deletions

View File

@@ -123,3 +123,17 @@ def test_yield_fixture_is_deprecated() -> None:
@pytest.yield_fixture
def fix():
assert False
def test_private_is_deprecated() -> None:
class PrivateInit:
def __init__(self, foo: int, *, _ispytest: bool = False) -> None:
deprecated.check_ispytest(_ispytest)
with pytest.warns(
pytest.PytestDeprecationWarning, match="private pytest class or function"
):
PrivateInit(10)
# Doesn't warn.
PrivateInit(10, _ispytest=True)

View File

@@ -621,7 +621,7 @@ class TestRequestBasic:
def test_func(something): pass
"""
)
req = fixtures.FixtureRequest(item)
req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.function == item.obj
assert req.keywords == item.keywords
assert hasattr(req.module, "test_func")
@@ -661,7 +661,9 @@ class TestRequestBasic:
)
(item1,) = testdir.genitems([modcol])
assert item1.name == "test_method"
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
arg2fixturedefs = fixtures.FixtureRequest(
item1, _ispytest=True
)._arg2fixturedefs
assert len(arg2fixturedefs) == 1
assert arg2fixturedefs["something"][0].argname == "something"
@@ -910,7 +912,7 @@ class TestRequestBasic:
def test_request_getmodulepath(self, testdir):
modcol = testdir.getmodulecol("def test_somefunc(): pass")
(item,) = testdir.genitems([modcol])
req = fixtures.FixtureRequest(item)
req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.fspath == modcol.fspath
def test_request_fixturenames(self, testdir):
@@ -1052,7 +1054,7 @@ class TestRequestMarking:
pass
"""
)
req1 = fixtures.FixtureRequest(item1)
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
assert "xfail" not in item1.keywords
req1.applymarker(pytest.mark.xfail)
assert "xfail" in item1.keywords
@@ -3882,7 +3884,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None:
@@ -3928,7 +3930,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
# order of fixtures based on their scope and position in the parameter list
assert (
request.fixturenames == "s1 my_tmpdir_factory p1 m1 f1 f2 my_tmpdir".split()
@@ -3954,7 +3956,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_scopes_reordered(self, testdir):
@@ -3987,7 +3989,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 m1 c1 f2 f1".split()
def test_func_closure_same_scope_closer_root_first(self, testdir):
@@ -4027,7 +4029,7 @@ class TestScopeOrdering:
}
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
def test_func_closure_all_scopes_complex(self, testdir):
@@ -4071,7 +4073,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
def test_multiple_packages(self, testdir):

View File

@@ -1156,7 +1156,7 @@ def test_gitignore(testdir):
from _pytest.cacheprovider import Cache
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path = cache._cachedir.joinpath(".gitignore")
@@ -1178,7 +1178,7 @@ def test_does_not_create_boilerplate_in_existing_dirs(testdir):
"""
)
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
assert os.path.isdir("v") # cache contents
@@ -1192,7 +1192,7 @@ def test_cachedir_tag(testdir):
from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT

View File

@@ -28,7 +28,7 @@ def test_recwarn_functional(testdir) -> None:
class TestWarningsRecorderChecker:
def test_recording(self) -> None:
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
assert not rec.list
warnings.warn_explicit("hello", UserWarning, "xyz", 13)
@@ -45,7 +45,7 @@ class TestWarningsRecorderChecker:
def test_warn_stacklevel(self) -> None:
"""#4243"""
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
warnings.warn("test", DeprecationWarning, 2)
@@ -53,21 +53,21 @@ class TestWarningsRecorderChecker:
from _pytest.recwarn import WarningsChecker
with pytest.raises(TypeError):
WarningsChecker(5) # type: ignore
WarningsChecker(5, _ispytest=True) # type: ignore[arg-type]
with pytest.raises(TypeError):
WarningsChecker(("hi", RuntimeWarning)) # type: ignore
WarningsChecker(("hi", RuntimeWarning), _ispytest=True) # type: ignore[arg-type]
with pytest.raises(TypeError):
WarningsChecker([DeprecationWarning, RuntimeWarning]) # type: ignore
WarningsChecker([DeprecationWarning, RuntimeWarning], _ispytest=True) # type: ignore[arg-type]
def test_invalid_enter_exit(self) -> None:
# wrap this test in WarningsRecorder to ensure warning state gets reset
with WarningsRecorder():
with WarningsRecorder(_ispytest=True):
with pytest.raises(RuntimeError):
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
rec.__exit__(None, None, None) # can't exit before entering
with pytest.raises(RuntimeError):
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
with rec:
pass # can't enter twice

View File

@@ -48,7 +48,9 @@ class FakeConfig:
class TestTempdirHandler:
def test_mktemp(self, tmp_path):
config = cast(Config, FakeConfig(tmp_path))
t = TempdirFactory(TempPathFactory.from_config(config))
t = TempdirFactory(
TempPathFactory.from_config(config, _ispytest=True), _ispytest=True
)
tmp = t.mktemp("world")
assert tmp.relto(t.getbasetemp()) == "world0"
tmp = t.mktemp("this")
@@ -61,7 +63,7 @@ class TestTempdirHandler:
"""#4425"""
monkeypatch.chdir(tmp_path)
config = cast(Config, FakeConfig("hello"))
t = TempPathFactory.from_config(config)
t = TempPathFactory.from_config(config, _ispytest=True)
assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()