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

Add type annotations #225

Closed
wants to merge 5 commits into from
Closed

Conversation

bluetech
Copy link
Member

Based on #224.

This PR adds type annotations to the library, using the Python 2-compatible syntax. The types are not exposed yet (a py.typed file is not distributed).

Motivation: while working on adding type annotations to pytest, I noticed that pluggy is a core part of its machinery and that having type annotations for it will be useful. However, the annotations here turn out to not be that useful, because the main interface, plugin_manager.hook.*, remains untyped (see #191). The API is very dynamic and doesn't lend itself easily to static annotations, though it might be possible, maybe with a mypy plugin. But the types are still useful for all of the regular non-dynamic API. They also help when trying to understand the code base - the type of each variable and how all of the types fit together. Finally, they help catch bugs when the code changes and add confidence when refactoring.

I tested it against pytest (feature branch): first that the test suite passes, and second that the type-checking passes (I manually added a py.typed file to pluggy in the venv to test that).

This PR is probably not that easy to review, adds some maintenance burden, and I also didn't discuss it first. If you don't feel type annotations are desirable for pluggy,, I'll understand :)

`defaults` is the default values e.g. `1` in `foo=1`. It's just used to
find the offset of the kwargs, but the code reused the name which made
me scratch my head for a minute.
The test passes invalid values to the instance (it does not takes
functions, it takes HookImpls). The test fails, but it accidentally used
`return` instead of `assert` so it wasn't visible.

Since what is being tested is evidently unimportant and legacy, just
remove it.
It doesn't take `**kwargs` but an argument `kwargs`.
@codecov-io
Copy link

Codecov Report

Merging #225 into master will decrease coverage by 4.84%.
The diff coverage is 63.82%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #225      +/-   ##
==========================================
- Coverage   93.88%   89.04%   -4.85%     
==========================================
  Files           9       14       +5     
  Lines        1129     1826     +697     
  Branches       21      124     +103     
==========================================
+ Hits         1060     1626     +566     
- Misses         66      185     +119     
- Partials        3       15      +12
Impacted Files Coverage Δ
testing/test_details.py 91.39% <100%> (+0.09%) ⬆️
testing/test_invocations.py 98.61% <100%> (ø) ⬆️
testing/test_multicall.py 99.27% <100%> (+0.65%) ⬆️
testing/test_hookcaller.py 82.55% <100%> (+0.35%) ⬆️
src/pluggy/_tracing.py 80% <31.81%> (ø)
testing/test_deprecations.py 91.17% <50%> (ø) ⬆️
src/pluggy/callers.py 70.73% <52%> (ø)
src/pluggy/hooks.py 83.33% <64.63%> (ø)
testing/test_helpers.py 80.7% <66.66%> (-1.12%) ⬇️
testing/test_tracer.py 98.61% <66.66%> (-1.39%) ⬇️
... and 7 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8aa5592...97a743e. Read the comment docs.

@goodboy
Copy link
Contributor

goodboy commented Jul 24, 2019

@bluetech definitely something I'm interested in introducing though I don't have time to look through it right now.

I will get to it; feel free to incrementally ping if I take too long.

@goodboy goodboy requested review from nicoddemus, asottile, goodboy and RonnyPfannschmidt and removed request for nicoddemus and asottile July 24, 2019 01:42
from .manager import PluginManager

_Writer = Callable[[str], None]
_Processor = Callable[[Tuple[str, ...], Sequence[object]], None]
Copy link
Member

Choose a reason for hiding this comment

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

should this be Sequence[Any]?

Copy link
Member

Choose a reason for hiding this comment

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

(and throughout)

Copy link
Member Author

Choose a reason for hiding this comment

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

As you probably know, object and Any both accept any type, but they are opposites in terms of what you can do with them: with object, you can't do much of anything (just __str__ and stuff) without narrowing it first with isinstance, while Any permits anything.

The rule I usually follow is: for inputs that can be anything, I use object - this puts more restrictions on me and provides more assurances to the caller. For outputs, I use Any, since I don't want to force the caller to perform isinstance checks.

(Note: when the use provides a callback to be called by the library, the roles reverse, that is the arguments become Any and the return type becomes object).

I probably got it wrong in a few places so I'll go over it.

self.root = root
self.tags = tags

def __call__(self, *args):
# type: (object) -> None
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be # type: (*Any) -> None

def __getitem__(self, parameter): # type: ignore
return object

Generic = _GenericMeta() # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

hmm I wonder if we shouldn't just add a dep on typing for py2 and avoid some/most of these (albeit interesting!) fakes

Copy link
Member Author

Choose a reason for hiding this comment

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

I think these are minor enough that a dependency is not worth it. That said, for me personally py2 is ancient history and the dep would certainly make things cleaner. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

my vote is the typing;python_version="2.7"

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I'll switch to that.

One reason I forgot to mention is that putting everything under if False avoids bringing in the heavy typing import at runtime. But I suppose pytest already "pays" that so it's all the same.

"""Get the result(s) for this hook call.

If the hook was marked as a ``firstresult`` only a single value
will be returned otherwise a list of results.
"""
__tracebackhide__ = True
if self._excinfo is None:
return self._result
return self._result # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

is the # type: ignore because we know it can't be None at this point?

Copy link
Member Author

Choose a reason for hiding this comment

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

We know it has type _T. However _T itself can be NoneType so an assert is not None is not possible. Will add a comment.

BTW here is some interesting discussion about a nicer Result type with mypy: python/mypy#6936


try: # func MUST be a function or method here or we won't parse any args
spec = _getargspec(func)
if hasattr(inspect, "getfullargspec"):
Copy link
Member

Choose a reason for hiding this comment

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

if you have if sys.version_info > (...): instead then the type annotation won't be needed here since mypy will know from typeshed

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll change that.

if inspect.ismethod(func) or (
"." in getattr(func, "__qualname__", ()) and args[0] in implicit_names
):
qualname = getattr(func, "__qualname__", "") # type: str
Copy link
Member

Choose a reason for hiding this comment

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

hmm why was the annotation necessary here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Otherwise it'd have type Any.

Copy link
Member

Choose a reason for hiding this comment

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

is that an error? sounds like the settings are cranked to an annoying level then :P

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not an error, just mypy doesn't know the type so we need to tell it. Can be done without the type comment if we use an hasattr or a try-except but the little comment seemed better.


def __getattr__(self, attr, default=None):
def __getattr__(self, attr, default=None): # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

what's the # type: ignore here?

Copy link
Member Author

Choose a reason for hiding this comment

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

warn_return_any: Returning Any from a function declared to return "str".

disallow_any_unimported = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_untyped_defs = True
Copy link
Member

Choose a reason for hiding this comment

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

I find most of the any settings more annoying than they are helpful -- they also tend to lead to lower quality annotations in my experience 🤷‍♂

Copy link
Member Author

Choose a reason for hiding this comment

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

Even after everything is annotated?

@bluetech
Copy link
Member Author

I created two PRs #226 and #227 which will make this PR a bit simpler. I'll wait on them before I update this PR.

@bluetech
Copy link
Member Author

This is blocked on some other work, I'll close this now - can be resubmitted later.

@jtpavlock
Copy link

Is this still blocked? I imagine a lot of the comment annotations could be changed to the normal syntax as indicated in #267

@bluetech
Copy link
Member Author

bluetech commented Aug 8, 2020

I rebased this branch now: https://github.com/bluetech/pluggy/commits/type-annotations

I think it's good but still needs some self-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants