Skip to content

Fix remaining typing issues for PyLinter and related functions #5663

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

Merged
merged 5 commits into from
Feb 8, 2022

Conversation

DanielNoord
Copy link
Collaborator

  • Add yourself to CONTRIBUTORS if you are a new contributor.
  • Write a good description on what the PR does.

Type of Changes

Type
🔨 Refactoring

Description

These are the final changes necessary to add typing to the __init__ of PyLinter.
I'll comment on some of the changes as these are a bit all over the place.

@DanielNoord DanielNoord added the Maintenance Discussion or action around maintaining pylint or the dev workflow label Jan 11, 2022
@@ -24,7 +30,7 @@
# The line/node distinction does not apply to fatal errors and reports.
_SCOPE_EXEMPT = "FR"

MSG_TYPES = {
MSG_TYPES: Final = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Although mypy doesn't recognise that because this dict is final its values could be seen as Literal instead of str it does "allow" us to add an ignore later on.
We will be warned if MSG_TYPES is being altered and we can thus be sure that any of the values obtained from it is one of the values already in there.

Copy link
Member

Choose a reason for hiding this comment

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

There isn't really a concept of final dicts that I know of (at this time). However there might be two separate workaround that you could try.

  1. Dict[Literal["I", "C", ...], Literal["info", "convention", ...] That should work but doesn't guarantee a mapping between specific keys and values.
  2. If the mapping is important, I would suggest creating a TypedDict and using that instead of Final for the annotation. That only works for string keys, but in this case it could be exactly what you're looking for. (It might even be worth a suggestion on the mypy / pyright issue tracker?)
class M(TypedDict):
    I: Literal["info"]
    C: Literal["converntion"]
    ...

Copy link
Member

Choose a reason for hiding this comment

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

It could be that you then need a cast for message_definition.msgid[0] though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@cdce8p Do you know if there is any plans or previous discussion about the concept of final dicts? I saw a similar issue about final frozen sets I think, which would also be beneficial to us.

I can change this to TypedDict, but if there are plans to (at some point) support final dicts I think it makes sense to keep it like this as it "better" represents what this actually is.

Copy link
Member

Choose a reason for hiding this comment

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

@cdce8p Do you know if there is any plans or previous discussion about the concept of final dicts? I saw a similar issue about final frozen sets I think, which would also be beneficial to us.

I can change this to TypedDict, but if there are plans to (at some point) support final dicts I think it makes sense to keep it like this as it "better" represents what this actually is.

I don't know of any plans. If TypedDicts work, they might be the easiest solution here.

@@ -537,7 +537,7 @@ def make_options() -> Tuple[Tuple[str, OptionDict], ...]:
def __init__(
self,
options=(),
reporter=None,
reporter: Union[reporters.BaseReporter, reporters.MultiReporter, None] = 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.

I wanted to do this in a separate PR but some of the issues fixed here didn't show if I didn't add typing to at least one of these parameters.

@@ -1496,7 +1496,11 @@ def _add_one_message(
msg_cat = MSG_TYPES[message_definition.msgid[0]]
self.msg_status |= MSG_TYPES_STATUS[message_definition.msgid[0]]
self.stats.increase_single_message_count(msg_cat, 1)
self.stats.increase_single_module_message_count(self.current_name, msg_cat, 1)
self.stats.increase_single_module_message_count(
self.current_name, # type: ignore[arg-type] # Should be removable after https://github.com/PyCQA/pylint/pull/5580
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

current_name shouldn't be None...

@@ -1613,6 +1617,9 @@ def _set_one_msg_status(
) -> None:
"""Set the status of an individual message"""
if scope == "module":
if not isinstance(line, int):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

int is never None if the scope is module. I'm not sure what the best way to do this is. If it is not an int we should indeed raise a ValueError, but in our current code that's impossible...
Perhaps an assert? Let me know!

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 asserts are the canonical way to convey "I'm doing that to appease the mypy god".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, although both assert and isinstance will actually decrease performance. I wonder if there is a way to do this without impacting performance..

Copy link
Member

Choose a reason for hiding this comment

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

Some typing can be declared beforehand at no cost (I think we used that for the message id store). But it only work when it's actually impossible for line to not be an int. I think the only way is to change the design which might be costly (and/or overkill).

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 know that it's never None, just use cast.
In case it's supposed to always be int, assert isinstance(line, int) is probably the better choice. The assert basically checks that some other code didn't mess up.

if isinstance(...) is more designed to select a code branch in case of true alternatives.

@@ -139,6 +139,8 @@ def handle_ignored_message(
or globally.
"""
if state_scope == MSG_STATE_SCOPE_MODULE:
if not isinstance(line, int):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Similar problem here: if state_scope == MSG_STATE_SCOPE_MODULE then line is never not an int. How to fix this...?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't it an issue to fix in astroid ?

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 this is a pylint issue. If we disable something via pylintrc its disabled on the "package" scope, that's why it doesn't have a line number. If we disable something in a module itself it will always have an assosciated line number

Copy link
Member

Choose a reason for hiding this comment

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

All right, thank you for clearing that up.

Maybe we could create a different structure to handle module and package disable, but I'm not very clear on the state of the code right now. I'll have a look before 2.13.0 is released. (Or I trust you if you find a solution that you think is good enough in the meantime).

Copy link
Member

Choose a reason for hiding this comment

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

Same here. Either cast or possibly assert.

@coveralls
Copy link

coveralls commented Jan 11, 2022

Pull Request Test Coverage Report for Build 1800595762

  • 10 of 10 (100.0%) changed or added relevant lines in 5 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.01%) to 93.932%

Totals Coverage Status
Change from base Build 1791297472: 0.01%
Covered Lines: 14845
Relevant Lines: 15804

💛 - Coveralls

@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.13.0 milestone Jan 11, 2022
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

I think we can merge, the isinstance won't be the main contributor to pylint being slow and a refactor of the design would take a long time. We can benefit from the typing before that.

@DanielNoord
Copy link
Collaborator Author

Perhaps @cdce8p knows a good trick for this?

@cdce8p
Copy link
Member

cdce8p commented Feb 2, 2022

Perhaps @cdce8p knows a good trick for this?

I left some comments. Check the conversations above 🔝

@@ -24,7 +30,7 @@
# The line/node distinction does not apply to fatal errors and reports.
_SCOPE_EXEMPT = "FR"

MSG_TYPES = {
MSG_TYPES: Final = {
Copy link
Member

Choose a reason for hiding this comment

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

@cdce8p Do you know if there is any plans or previous discussion about the concept of final dicts? I saw a similar issue about final frozen sets I think, which would also be beneficial to us.

I can change this to TypedDict, but if there are plans to (at some point) support final dicts I think it makes sense to keep it like this as it "better" represents what this actually is.

I don't know of any plans. If TypedDicts work, they might be the easiest solution here.

@@ -1631,6 +1635,8 @@ def _set_one_msg_status(
) -> None:
"""Set the status of an individual message"""
if scope == "module":
assert isinstance(line, int)
Copy link
Member

Choose a reason for hiding this comment

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

I was wondering, would it make sense to add a comment along the lines

assert ...  # line should always be int inside module scope

@@ -139,6 +139,8 @@ def handle_ignored_message(
or globally.
"""
if state_scope == MSG_STATE_SCOPE_MODULE:
assert isinstance(line, int)
Copy link
Member

Choose a reason for hiding this comment

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

Same here

@DanielNoord
Copy link
Collaborator Author

TypedDict creates a lot of problems because you can only use the correct keys and its difficult to assert that the first letter of each message id is in the keys of the TypedDict. I have opted to leave it as is.

@DanielNoord DanielNoord merged commit 9989444 into pylint-dev:main Feb 8, 2022
@DanielNoord DanielNoord deleted the typing-pylinter branch February 8, 2022 21:00
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants