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

Finish typing of pylint.pyreverse.utils #6549

Merged
merged 7 commits into from
May 11, 2022

Conversation

DudeNr33
Copy link
Collaborator

@DudeNr33 DudeNr33 commented May 8, 2022

  • Write a good description on what the PR does.
  • If you used multiple emails or multiple names when contributing, add your mails
    and preferred name in script/.contributors_aliases.json

Type of Changes

Type
🔨 Refactoring

Description

This module already had some typing, added typing to the classes and functions that did not have any yet.

Running with mypy --strict four issues remain, which I can't wrap my head around why mypy complains about them:

pylint [typing-pyreverse-p3] % mypy --strict pylint/pyreverse/utils.py           
pylint/pyreverse/utils.py:72: error: Returning Any from function declared to return "bool"  [no-any-return]
pylint/pyreverse/utils.py:77: error: Returning Any from function declared to return "bool"  [no-any-return]
pylint/pyreverse/utils.py:213: error: Returning Any from function declared to return "str"  [no-any-return]
pylint/pyreverse/utils.py:215: error: Returning Any from function declared to return "str"  [no-any-return]
Found 4 errors in 1 file (checked 1 source file)

@DudeNr33 DudeNr33 added pyreverse Related to pyreverse component typing Maintenance Discussion or action around maintaining pylint or the dev workflow labels May 8, 2022
@DudeNr33 DudeNr33 added this to the 2.15.0 milestone May 8, 2022
Comment on lines -63 to -78
ABSTRACT = re.compile(r"^.*Abstract.*")
FINAL = re.compile(r"^[^\W\da-z]*$")


def is_abstract(node):
"""Return true if the given class node correspond to an abstract class
definition.
"""
return ABSTRACT.match(node.name)


def is_final(node):
"""Return true if the given class/function node correspond to final
definition.
"""
return FINAL.match(node.name)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dead code -> removed instead of adding typing

# bw compatibility
return node.type == "exception"


# Helpers #####################################################################

_CONSTRUCTOR = 1
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dead code

Comment on lines 154 to 156
) -> tuple[
Callable[[nodes.NodeNG], Any] | None, Callable[[nodes.NodeNG], Any] | None
]:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There must be a better way to make a shorthand notation for it (maybe by creating a TypeVar?), but I could not come up with it.
Maybe one of the typing pros @DanielNoord or @cdce8p can help?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can use:
MyRecurringType = Callable[[int, int, int, int, int, int, ...], float] at the top of the file just below the imports and then re-use it throughout the file.
The only thing you need to look out for is that Callable from collections.abc then needs to be imported from typing. As this way MyRecurringType is defined at runtime and on <3.8 Callable isn't subscriptable at runtime.

Copy link
Member

Choose a reason for hiding this comment

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

For the callable parameter type, take a look at my comment here as it's not completely correct (but probably the most practical):
https://github.com/PyCQA/pylint/blob/8df920b341a872b85754c910598d27a8846988f9/pylint/utils/ast_walker.py#L18-L24

--
As a side note: Shouldn't the return type be None instead of Any?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you two for the suggestions! I somehow thought just declaring a variable with the type might not work, but I should have probably just tried it. 😄

@cdce8p in most cases, yes - but pyreverse is a bit special here: LocalVisitor.visit(self, node) returns the result of the leave_* method to the caller. In practice, this will be the result from DefaultDiadefGenerator.leave_project(self, _), which is a tuple of ClassDiagram and PackageDiagram.
This could surely be made nicer with a future refactor.

@coveralls
Copy link

coveralls commented May 8, 2022

Pull Request Test Coverage Report for Build 2308294733

  • 17 of 17 (100.0%) changed or added relevant lines in 1 file are covered.
  • 78 unchanged lines in 11 files lost coverage.
  • Overall coverage decreased (-0.001%) to 95.344%

Files with Coverage Reduction New Missed Lines %
pylint/checkers/refactoring/refactoring_checker.py 1 98.37%
pylint/testutils/functional/test_file.py 1 96.49%
pylint/checkers/classes/special_methods_checker.py 2 94.83%
pylint/lint/utils.py 2 95.45%
pylint/checkers/exceptions.py 3 97.7%
pylint/utils/utils.py 3 86.93%
pylint/utils/file_state.py 5 95.24%
pylint/lint/message_state_handler.py 7 96.5%
pylint/checkers/utils.py 10 95.54%
pylint/testutils/lint_module_test.py 21 87.3%
Totals Coverage Status
Change from base Build 2289989458: -0.001%
Covered Lines: 16033
Relevant Lines: 16816

💛 - Coveralls



def is_interface(node):
def is_interface(node: nodes.ClassDef) -> bool:
# bw compatibility
return node.type == "interface"
Copy link
Member

Choose a reason for hiding this comment

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

So, here, mypy thinks we return "Any" ?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, no clue why. Maybe because the type attribute is monkey-patched on the node class.
But even VS Code recognises node.type correctly as str.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you, that explains why this happens here.
On lines 213 and 215 I still don't know why mypy complains here:

def get_annotation_label(ann: nodes.Name | nodes.NodeNG) -> str:
    if isinstance(ann, nodes.Name) and ann.name is not None:
        return ann.name. # <-- l.213
    if isinstance(ann, nodes.NodeNG):
        return ann.as_string()  # <-- l.215
    return ""

Using typing.reveal_type(), both ann.name and ann.as_string() are recognised as str, not Any.

Copy link
Member

Choose a reason for hiding this comment

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

Although we have already added some annotations to astroid, mypy will not use them until we add a py.typed file (once they are done). Until then all astroid types are basically inferred as Any.

Copy link
Member

Choose a reason for hiding this comment

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

self._cache: dict[
type[nodes.NodeNG],
tuple[
Callable[[nodes.NodeNG], Any] | None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we know what these Callable return?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As mentioned above: most of the time None, but for the one leave_project method it is a tuple of ClassDiagram and optionally PackageDiagram. So a more precise type would be:
Callable[[nodes.NodeNG], None | tuple[ClassDiagram] | tuple[PackageDiagram, ClassDiagram]]

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd be in favour of using the narrowed down type if we can. We can always change it after a refactor later.


def visit(self, node):
def visit(self, node: nodes.NodeNG) -> Any:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this truly Any? Don't all visit_ methods return None?

Copy link
Member

Choose a reason for hiding this comment

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

With the updated typing, should this be Union[Tuple[ClassDiagram], Tuple[PackageDiagram, ClassDiagram], None]?

If that's the case, the visit method is probably not type compatible with the base implementation in ASTWalker.visit with is annotated to only return None. The type of the child implementation should be a subclass of the parent impl.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. I can update both methods to use Union[Tuple[ClassDiagram], Tuple[PackageDiagram, ClassDiagram], None].

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On second thought:
This also comes down to design problems.
ASTWalker.visit does in fact always return None (there is no return inside the method).
LocalsVisitor (a subclass of ASTWalker) overrides visit and returns whatever the leave method for this node type returns. That's why I put Any here in the first place.

Currently the ASTWalker class is never used on its own. We could simply remove it and move the relevant methods into LocalsVisitor.

Copy link
Member

Choose a reason for hiding this comment

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

That should work. Alternatively, ASTWalker.visit can be annotated with the return type from LocalsVisitor.visit. Doesn't matter that only None is actually returned there. But with that the subclass implementation is compatible.

@DudeNr33
Copy link
Collaborator Author

I'm trying to incorporate the last changes requested by @DanielNoord, but I'm running into strange problems.

I introduced a variable for the callback types in the if TYPE_CHECKING block:

if TYPE_CHECKING:
    # pylint: disable=unsupported-binary-operation
    from pylint.pyreverse.diadefslib import DiaDefGenerator
    from pylint.pyreverse.diagrams import ClassDiagram, PackageDiagram
    from pylint.pyreverse.inspector import Linker

    _CallbackT = Callable[
        [nodes.NodeNG], tuple[ClassDiagram] | tuple[PackageDiagram, ClassDiagram] | None
    ]
    _CallbackTupleT = tuple[_CallbackT | None, _CallbackT | None]

When I stage those changes and run pre-commit run, everything is fine (also if I run mypy pylint/pyreverse directly).

pylint [typing-pyreverse-p3●●] % pre-commit run
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
autoflake................................................................Passed
copyright-notice.........................................................Passed
pyupgrade................................................................Passed
isort....................................................................Passed
black....................................................................Passed
black-disable-checker....................................................Passed
flake8...................................................................Passed
pylint...................................................................Passed
Fix documentation....................................(no files to check)Skipped
rstcheck.............................................(no files to check)Skipped
mypy.....................................................................Passed
prettier.................................................................Passed
pydocstringformatter.....................................................Passed

However, as soon as I run git commit, I get tons of errors:

pylint [typing-pyreverse-p3●] % git commit
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
autoflake................................................................Passed
copyright-notice.........................................................Passed
pyupgrade................................................................Passed
isort....................................................................Passed
black....................................................................Passed
black-disable-checker....................................................Passed
flake8...................................................................Passed
pylint...................................................................Passed
Fix documentation....................................(no files to check)Skipped
rstcheck.............................................(no files to check)Skipped
mypy.....................................................................Failed
- hook id: mypy
- exit code: 1

pylint/pyreverse/utils.py:25: error: Invalid type alias: expression is not a valid type  [misc]
pylint/pyreverse/utils.py:26: error: Type application has too many types (1 expected)  [misc]
pylint/pyreverse/utils.py:28: error: Type expected within [...]  [misc]
pylint/pyreverse/utils.py:28: error: Invalid type alias: expression is not a valid type  [misc]
pylint/pyreverse/utils.py:28: error: The type "Type[Tuple[Any, ...]]" is not generic and not indexable  [misc]
pylint/pyreverse/utils.py:28: error: Unsupported left operand type for | ("object")  [operator]
pylint/pyreverse/utils.py:136: error: Variable "pylint.pyreverse.utils._CallbackTupleT" is not valid as a type  [valid-type]
pylint/pyreverse/utils.py:136: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
pylint/pyreverse/utils.py:155: error: Variable "pylint.pyreverse.utils._CallbackTupleT" is not valid as a type  [valid-type]
pylint/pyreverse/utils.py:155: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
pylint/pyreverse/utils.py:175: error: Value of type _CallbackTupleT? is not indexable  [index]
pylint/pyreverse/utils.py:181: error: Value of type _CallbackTupleT? is not indexable  [index]
pylint/pyreverse/utils.py:200: error: Value of type _CallbackTupleT? is not indexable  [index]
pylint/pyreverse/utils.py:201: error: Value of type _CallbackTupleT? is not indexable  [index]
pylint/pyreverse/utils.py:205: error: Value of type _CallbackTupleT? is not indexable  [index]
pylint/pyreverse/utils.py:206: error: Value of type _CallbackTupleT? is not indexable  [index]
Found 14 errors in 1 file (checked 1 source file)

prettier.................................................................Passed
pydocstringformatter.....................................................Passed

Shouldn't the results be the same?

@cdce8p
Copy link
Member

cdce8p commented May 10, 2022

I'm trying to incorporate the last changes requested by @DanielNoord, but I'm running into strange problems.

Try Tuple[..] with a capital first letter. Support for tuple[..] was added in Python 3.9. It's possible to use it in string annotations since they aren't parsed and thus don't need to be valid Python code.

@DudeNr33
Copy link
Collaborator Author

DudeNr33 commented May 10, 2022

I think as those lines are in the if TYPE_CHECKING block we should not have any runtime problems anyway, right?
But thanks for the hint, I changed it accordingly. 👍

I could solve my pre-commit problems by reinstalling the pre-commit hooks (pre-commit uninstall, then pre-commit install).
Ok I'm completely lost now. Now the errors show up in CI, but not on my own machine.
mypy version in the pre-commit hooks are both v0.950 in CI and my laptop.

Might be a caching problem? The CI re-uses a cached environment for mypy, and the problems on my own machine were resolved after re-installing the hooks and therefore probably also deleting the existing cache.

@cdce8p
Copy link
Member

cdce8p commented May 10, 2022

I think as those lines are in the if TYPE_CHECKING block we should not have any runtime problems anyway, right? But thanks for the hint, I changed it accordingly. 👍

Yeah, technically true. I'm not sure though mypy knows that the type alias is defined in a TYPE_CHECKING block. Probably not yet implemented.

I could solve my pre-commit problems by reinstalling the pre-commit hooks (pre-commit uninstall, then pre-commit install). Ok I'm completely lost now. Now the errors show up in CI, but not on my own machine. mypy version in the pre-commit hooks are both v0.950 in CI and my laptop.

Similar issue with PEP 604, i.e. Union operator. Use Union and Optional instead that should fix it.

pylint/pyreverse/utils.py Outdated Show resolved Hide resolved
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
@DudeNr33
Copy link
Collaborator Author

Thanks a lot for your help, that would have cost me ages to figure out on my own.


_CallbackT = Callable[
[nodes.NodeNG],
Union[Tuple[ClassDiagram], Tuple[PackageDiagram, ClassDiagram], None],
Copy link
Member

Choose a reason for hiding this comment

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

Returning tuples of different lengths is probably not the best API design

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No doubt about that. The code where this happens is over 16 years old, and I haven't touched those parts of pyreverse yet. I can take a look at refactoring those parts that we found to be problematic due to the typing in a follow-up PR.

pylint/pyreverse/utils.py Outdated Show resolved Hide resolved

def visit(self, node):
def visit(self, node: nodes.NodeNG) -> Any:
Copy link
Member

Choose a reason for hiding this comment

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

With the updated typing, should this be Union[Tuple[ClassDiagram], Tuple[PackageDiagram, ClassDiagram], None]?

If that's the case, the visit method is probably not type compatible with the base implementation in ASTWalker.visit with is annotated to only return None. The type of the child implementation should be a subclass of the parent impl.

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

I'll let you decide how to handle the Any. Rest LGTM

@@ -121,7 +115,7 @@ def __init__(self, mode):
print(f"Unknown filter mode {ex}", file=sys.stderr)
self.__mode = __mode

def show_attr(self, node):
def show_attr(self, node: nodes.NodeNG | str) -> bool:
"""Return true if the node should be treated."""
visibility = get_visibility(getattr(node, "name", node))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
visibility = get_visibility(getattr(node, "name", node))
visibility = get_visibility(getattr(node, "name", node))

Unrelated, but this is quite a funny hack 😄

@DudeNr33
Copy link
Collaborator Author

I'll let you decide how to handle the Any. Rest LGTM

I annotated both parent and child implementation with -> Any - I don't really see the need to put perfect typing here if we are going to refactor this anyway.
I created #6582, and would first collect all issues we find while completing the typing, and then work on the refactoring.

@DudeNr33 DudeNr33 merged commit e91a674 into pylint-dev:main May 11, 2022
@DudeNr33 DudeNr33 deleted the typing-pyreverse-p3 branch May 11, 2022 19:34
@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 2.15.0, 2.14.0 May 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Maintenance Discussion or action around maintaining pylint or the dev workflow pyreverse Related to pyreverse component typing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants