Repository navigation
Typing and public API #7469
Description
pytest's "official" public API is exported by the pytest package, and everything else is defined in the _pytest package.
Currently, pytest only contains APIs that users need to use directly: functions to be called, decorators, classes to be instantiated or subclasssed, globals. Other objects, which the user doesn't interact with directly, are not exported.
Typing extends the range of cases in which some object needs to be referred to by name. As long as the user can somehow reach an object, directly or indirectly, through some public API, they need to be able to refer to its type by name, due to type annotations.
To come up with a list of reachable but unexported types, I used the following categories:
-
Fixtures: built-in fixtures that are dependency-injected. To benefit from the types the user needs to type annotate them, for example:
def test_it(monkeypatch: MonkeyPatch) -> None: ...
-
Hooks: the user needs to be able to type annotate the arguments and return values of any hook.
-
Transitive: types that are referred to transitively by some other public API, e.g. return value of a function, method, context manager, public attribute etc.
See below for the list I came up with.
Questions for discussion:
-
Is this a problem we want to fix?
-
Most of these types are classes, that are not intended to be instantiated and/or subclassed by users, only internally.
2.1. Do we want to enforce this?
2.2. How do we enforce this? -
How do we want to officialy declare something as public API?
-
Which types from the list below are we actually ready to export?
-
Is there a category or specific types I missed?
I'll add my own thoughts on these questions in a separate reply.
Fixtures:
-
_pytest.fixtures.FixtureRequest->pytest.FixtureRequest -
_pytest.fixtures.SubRequest -
_pytest.cacheprovider.Cache->pytest.Cache -
_pytest.capture.CaptureFixture->pytest.CaptureFixture -
_pytest.logging.LogCaptureFixture->pytest.LogCaptureFixture -
_pytest.pytester.Pytester->pytest.Pytester -
_pytest.pytester.Testdir->pytest.Testdir -
_pytest.tmpdir.TempdirFactory->pytest.TempdirFactory -
_pytest.tmpdir.TempPathFactory->pytest.TempPathFactory -
_pytest.monkeypatch.MonkeyPatch->pytest.MonkeyPatch -
_pytest.recwarn.WarningsRecorder->pytest.WarningsRecorder
Hooks:
-
_pytest.config.Config->pytest.Config -
_pytest.config.PytestPluginManager->pytest.PytestPluginManager -
_pytest.config.argparsing.Parser->pytest.Parser -
_pytest.reports.CollectReport->pytest.CollectReport -
(Made private Make PyCollector an implementation detail - don't use in hook type annotation #9264)_pytest.python.PyCollector -
_pytest.python.Metafunc->pytest.Metafunc -
_pytest.runner.CallInfo->pytest.CallInfo -
_pytest.reports.TestReport->pytest.TestReport -
_pytest.fixtures.SubRequest -
_pytest.fixtures.FixtureDef -
_pytest.terminal.TerminalReporter -
_pytest._code.code.ExceptionRepr -
_pytest.code.ExceptionInfo->pytest.ExceptionInfo
API:
-
_pytest.mark.structures.ParameterSet
Transitive:
-
_pytest.fixtures.FuncFixtureInfo(thruMetafunc) -
_pytest.fixtures.FixtureFunctionMarker -
_pytest.mark.structures.MarkGenerator->pytest.MarkGenerator -
_pytest.mark.structures.MarkDecorator->pytest.MarkDecorator -
_pytest.mark.structures.Mark->pytest.Mark -
- only exposed as a_pytest.mark.structures.NodeKeywords(thruNode)MutableMappinginstead. -
_pytest.code._code.TerminalRepr -
_pytest.python.CallSpec2 -
_pytest.python_api.ApproxBase->pytest.Approx(Approx type exposure #14164) -
- Not worth exposing, just a context manager._pytest.python_api.RaisesContext -
_pytest.recwarn.WarningsRecorder->pytest.WarningsRecorder -
_pytest._io.TerminalWriter -
_pytest.config.argparsing.OptionGroup->pytest.OptionGroup -
_pytest.pytester.ParsedCall->pytest.RecordedHookCall -
_pytest.pytester.HookRecorder->pytest.HookRecorder -
_pytest.pytester.RunResult->pytest.RunResult -
_pytest.pytester.LineMatcher->pytest.LineMatcher
Activity
bluetech commentedon Jul 9, 2020
Here are my thoughts on the questions:
Yes; otherwise users can't utilize types in these cases. Also, if we don't, people will be importing from
_pytestanyway, which we don't want to happen.More generally, I think it will be really good for us to clearly delineate what we consider public API and what is private. Currently it is somewhat murky which makes internal refactorings difficult.
I think we want to enforce it, because otherwise instantiation and subclassing are implicitly assumed to be allowed, and then we are locked into supporting them even when we don't want to.
My current intention is to steal trio's approach and see how it goes.
I think there should be three criteria:
pytest._or__prefix.By "it" I mean both the types themselves, and their contents (methods, attributes, etc.).
Some work will be required to align reality with these requirements...
The fixtures in the list look good to export, except maybe
_pytest.fixtures.FixtureRequest/_pytest.fixtures.SubRequestwhich I'm not super clear about myself.Stuff in the hooks needs to be exported; some require care however, and we might decide to leave unexported in the first pass.
Similar for the "Transitive" ones.
I think we can discuss the specifics in future PRs.
That's for others to answer :)
nicoddemus commentedon Jul 9, 2020
Thanks @bluetech for writing up this issue with so much details. 👍
My thoughts:
I agree that we don't want to allow instantiation and subclassing, specially because we might want to change
__init__methods later to include additional parameters to support new features.An idea would be to implement declare ABCs, and only export the ABCs. This I believe would remove the need to export the actual type, and hence the
__init__method. It would also be a double-checking of the public API, because changing only the implementation would upset the type-checker and bring to light unintentional changes in the API.The downside is that we would need to implement ABCs for all the objects you listed here, which is a bunch of work.
This looks very similar to the work done by @RonnyPfannschmidt in the
Node.from_parentendeavor.Those criteria look good to me!
bluetech commentedon Jul 10, 2020
Doing this would be nice in that it would really highlight public vs. private. However I don't think we want to go this way for these reasons:
nicoddemus commentedon Jul 10, 2020
All good points, I had a hunch it was a lot of work but hand't thought through the details, but you confirmed it. 😁
So I agree that ABCs is a no-go.
RonnyPfannschmidt commentedon Jul 10, 2020
How about having a export module for types which has an explicitly spec that anything not exported somewhere else is only intended for type declaration and won't have public constructors for now
gaborbernat commentedon Jul 21, 2020
What about using protocols instead of ABCs? And we only expose the protocols publicly 🤔 That could even be separated into a typing module:
This way the fixtures have a very explicit public API, however the implementation does not have to inherit from it. Having the typing namespace makes it explicit that it's not meant for instiation. E.g.:
asottile commentedon Jul 24, 2020
I like the idea of protocols as a public api, it gives us a little bit more flexibility on the implementation
the tricky parts become though:
(1) how to ensure we stay in line with the protocols (typing tests?)
(2) if we use the protocols internally, we'd probably have to cast to the concrete types occasionally to use the internal methods? this feels like it defeats some of the purposes of typing in the first place 🤔
gaborbernat commentedon Jul 24, 2020
I don't think you need to cast protocols, they should be replaceable automatically. You can test it by merging the protocol and implementation via retype into a temporary folder 🤔and running type check on that 🤔
asottile commentedon Jul 24, 2020
@gaborbernat the problem would be if we internally defined things like:
that would not type check because
other_internal_methodis not on the public protocolgaborbernat commentedon Jul 24, 2020
So we just internally always type as implementation? 🤷♂️
adamtheturtle commentedon Jul 29, 2020
I believe
_pytest.fixtures.SubRequestis what I would like to import for #3342 (comment):graingert commentedon Aug 28, 2020
@asottile
@altendky has come up with a neat trick with a decorator for each protocol
this works today
It's a bit of boilerplate so would be quite nice, and I assume a few dozen lines of code to do, as a mypy plugin:
53 remaining items
Merge #128
Merge #35 #36
Merge #31
The-Compiler commentedon Mar 31, 2023
Another public API which is currently not annotated at all:
pytest.approx. With all the different values it accepts, I guess it won't be easy to annotate, but I suppose it might still be possible to have something based on aTypeVaror so?chore(deps): update dependency pytest to v7 (#3)
Export `FixtureDef`
Export `FixtureDef`
adamjstewart commentedon May 4, 2024
The-Compiler commentedon May 4, 2024
@adamjstewart That's already public as
pytest.skip.Exception, though I'm not sure if that's documented anywhere.Document exceptions raised by exit, skip, xfail, fail, and importorskip
Document exceptions raised by exit, skip, xfail, fail, and importorsk…
thomthom commentedon Oct 25, 2024
I ran into this when I tried to enable
stricttypechecking in a project of mine where I use VSCode and Pylance. It complains aboutexpectedbeing of unknown type. Would annotatingobjectwork as a general type allowing usage in strict mode?I tried to make a partial stub for
approxin my project, but because pytest has apy.typedpylance won't apply partial stubs.finite-state-machine commentedon Jan 5, 2025
I'm finding type checkers don't really understand
FixtureRequest.node, as in the following code:Pyright's output:
Mypy (
--strict) handles this less badly, but still not well:Would it be reasonable to change the definition as follows?
Make PyCollector an implementation detail - don't use in hook type an…
Make PyCollector an implementation detail - don't use in hook type an…