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

Handle class decorators during typing checks. #3890

Merged
merged 7 commits into from Nov 21, 2020
Merged

Handle class decorators during typing checks. #3890

merged 7 commits into from Nov 21, 2020

Conversation

JulienPalard
Copy link
Contributor

@JulienPalard JulienPalard commented Oct 9, 2020

Steps

  • Add yourself to CONTRIBUTORS if you are a new contributor.
  • Add a ChangeLog entry describing what your PR does.
  • If it's a new feature or an important bug fix, add a What's New entry in doc/whatsnew/<current release.rst>.
  • Write a good description on what the PR does.

Description

I'm trying myself at fixing #3882, by handling the specific case where a decorator is a class. I bet this could be handled elsewere to be made more generic, but as a first contribution I'm starting slow.

Type of Changes

Type
🐛 Bug fix

Related Issue

Closes #3882

@coveralls
Copy link

Coverage Status

Coverage increased (+0.004%) to 90.725% when pulling 3e1cb07 on JulienPalard:issue3882 into 1a1dea5 on PyCQA:master.

@coveralls
Copy link

coveralls commented Oct 12, 2020

Coverage Status

Coverage increased (+0.004%) to 90.82% when pulling 9b7fbc3 on JulienPalard:issue3882 into 9a5e1b3 on PyCQA:master.

Copy link

@amyreese amyreese left a comment

Choose a reason for hiding this comment

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

Confirmed that this resolves the bug on my projects under Python 3.9.0, and the code looks good overall, though I'm not a maintainer, so others might have more suggestions. This does need a changelog entry though describing the fixes, and you should add yourself to CONTRIBUTORS.txt if you'd like the recognition.

Take a look at the contributors' guide: http://pylint.pycqa.org/en/latest/development_guide/contribute.html

@JulienPalard
Copy link
Contributor Author

@jreese Thanks for the feedback!

Copy link
Contributor

@hippo91 hippo91 left a comment

Choose a reason for hiding this comment

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

@JulienPalard thanks a lot for this large step toward the support of python3.9! 👍
I have some questions/remarks. Can you also add an entry in doc/whatsnew/2.6.rst please?

CONTRIBUTORS.txt Outdated Show resolved Hide resolved
if inferred is None or inferred is astroid.Uninferable:
return

if inferred.decorators:
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should enter in these lines only if the protocol is not supported for the current inferred node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially did that way because a node could support the protocol, but its decorator remove it.

Dumb example:

def deco(f):
    def wrapped():
        return "Hello"
    return wrapped

So I'd prefer to only trust the last decorator of the node than the node itself. But It's my first commit on pylint and first time I use astropy, so if you're sure, I'll gladly do as you feel :)

if inferred is None or inferred is astroid.Uninferable:
return

if inferred.decorators:
first_decorator = helpers.safe_infer(inferred.decorators.nodes[0])
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO we should not restrict to the first decorator but instead emit the message if none of the decorators enable the support of the protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The type of the object will be of the type of the outer decorator (the "first" one from top to bottom), as the result of other decorators being just passed as an argument to the last, so I though it was enough.

We have nothing prooving a decorator will return the object that it receive (most will not, most will just return a new callable).

So I think we should really just look at the first one (hope [0] is the top one, the outer one, I don't know how to call it) hope I did not get it wrong?

(When I say "outer" I think of decorators as applied manually:

@a
@b
@c
def foo(): ...

being foo = a(b(c(foo))), so when I say "outer decorator" I mean a in this case, I don't know if there's a better wording?)

Copy link
Contributor

Choose a reason for hiding this comment

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

@JulienPalard i think you are right. Let's do as you think.

Comment on lines +1732 to +1733
return # It would be better to handle function
# decorators, but let's start slow.
Copy link
Contributor

@hippo91 hippo91 Oct 30, 2020

Choose a reason for hiding this comment

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

No need to handle this for now. We are not sure it will be useful one day and if it become the case it will be easy to add lines here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's why I added a comment, to guide future implementers it's here that it should be implemented. But I can reword this more lightly like "If we need to handle function decorators, do it here"? Or I can drop the comment if you prefer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it could be reworded. Maybe something along the lines: If necessary handling function decorators should be here

else:
return # It would be better to handle function
# decorators, but let's start slow.

Copy link
Contributor

Choose a reason for hiding this comment

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

The following lines take into account my remarks. Fill free to use it as it is or change everything. It is just a suggestion.

Suggested change
if supported_protocol(inferred):
return
if inferred.decorators:
# It there are decorators, maybe one of them enable the support of the protocol
for deco_node in inferred.decorators.nodes:
decorator = helpers.safe_infer(deco_node)
if (isinstance(decorator, astroid.ClassDef) and
supported_protocol(decorator.instantiate_class())):
return
# The protocol is not supported neither by the node nor through the decorators
self.add_message(msg, args=node.value.as_string(), node=node.value)

)
):
self.checker.visit_subscript(subscript)

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could add a test with a function decorated by more than one decorator and with one of them that enable the protocol?
What do you think about it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should also test the case where a subscriptable class is decorated by an unsubscriptable decorator.
Something along the lines:

    def test_subscriptable_decorated_by_an_unsubscriptable_class_issue3882(self):
        module = astroid.parse(
            """
        class Deco:
            def __init__(self, f):
                self.f = f

        @Deco
        class Decorated:
            def __getitem__(self, item):
                return item

        test = Decorated()[None]
        """
        )
        subscript = module.body[-1].value
        with self.assertNoMessages():
            self.checker.visit_subscript(subscript)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we could add a test with a function decorated by more than one decorator and with one of them that enable the protocol?
What do you think about it?

I'm not 100% sure I understand correctly when you say "one of them that enable the protocol", for me it depends too much on the next decorators that can easily "re-drop" the protocol by simply returning a function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think your example should typically message an error, in this specific case it should warn than Decorated(), as being an instance of Deco is not callable.

If we then add a __call__ to Deco that returns something that does not implement the protocol, the Deco removed the protocol from Decorated, and we should say so (but it's probably a whole other level of complexity).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, forget the case with multiple decorators.

Copy link
Contributor

Choose a reason for hiding this comment

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

Of forget also my suggestion. It was not pertinent.

@hippo91 hippo91 self-assigned this Oct 30, 2020
@dalemyers
Copy link

Is there anything I can do to help get this merged? This is a blocker for Python 3.9 for us, so I'd like to do whatever I can to get this merged.

@JulienPalard
Copy link
Contributor Author

@dalemyers Thanks for hopping in! Any help gladly appreciated as it's my very first contribution to this project, anything can go wrong.

  • You can first read my code, in case you find something strange.
  • You can test it locally (clone it and pip install . in a venv) to check if this PR actually fixes your issues.
  • You can tell us if you think we should check if anything in the whole stack of decorators adds the protocol as mentionned by @hippo91, or if only the last decorator should be analysed like I did (read the comments about this discussion).

@JulienPalard
Copy link
Contributor Author

I added some tests, I now test those cases:

@Subscriptable
def ...
@Unsubscriptable
def ...
@Subscriptable
@Unsubscriptable
def ...
@Unsubscriptable
@Subscriptable
def ...

@JulienPalard
Copy link
Contributor Author

Also checked that the tests don't pass without my patch. Actually some of them pass without my patch, which make sense:

  • decorated_by_an_unsubscriptable_class
  • decorated_by_subscriptable_then_unsubscriptable_class

as they would "false positive" (checking the function was unsubscriptable) before and "true positive" (checking the top most decorator is unsubscriptable) now.

@NeilGirdhar
Copy link

NeilGirdhar commented Nov 15, 2020

For anyone who wants to test this, "pip install git+https://github.com/JulienPalard/pylint@issue3882".

My results: this pull request solves the problem for me, looking forward to its inclusion!

@JulienPalard
Copy link
Contributor Author

For anyone who wants to test this, "pip install git+https://github.com/JulienPalard/pylint@issue3882".

I always forgot this, it's so great!

My results: this pull request solves the problem for me, looking forward to its inclusion!

\o/

Hope it breaks nothing (that would not be in the test suite :p).

@shadchin
Copy link

I have next trace (I can't show source code, sorry):

pylint/lint/run.py:349: in __init__
    linter.check(args)
pylint/lint/pylinter.py:862: in check
    self._check_files(
pylint/lint/pylinter.py:896: in _check_files
    self._check_file(get_ast, check_astroid_module, name, filepath, modname)
pylint/lint/pylinter.py:922: in _check_file
    check_astroid_module(ast_node)
pylint/lint/pylinter.py:1054: in check_astroid_module
    retval = self._check_astroid_module(
pylint/lint/pylinter.py:1099: in _check_astroid_module
    walker.walk(ast_node)
pylint/utils/ast_walker.py:75: in walk
    self.walk(child)
pylint/utils/ast_walker.py:75: in walk
    self.walk(child)
pylint/utils/ast_walker.py:75: in walk
    self.walk(child)
pylint/utils/ast_walker.py:72: in walk
    callback(astroid)
pylint/checkers/typecheck.py:1728: in visit_subscript
    if inferred.decorators:
E   AttributeError: 'Module' object has no attribute 'decorators'

Replace if inferred.decorators: on if getattr(inferred, "decorators", None): works fine.

@JulienPalard
Copy link
Contributor Author

Thanks a lot for testing it, and for the stacktrace!

Was able to reproduce it easily:

import typing

print(typing[True])

I'll add a test, and fix it.

@JulienPalard
Copy link
Contributor Author

/home/travis/build/PyCQA/pylint/tests/checkers/unittest_typecheck.py:43:0: R0904: Too many public methods (26/25) (too-many-public-methods)

is it telling me I wrote too much tests? :)

@pawelrubin
Copy link

Hi folks! When can we expect the next release including this fix?

Copy link
Contributor

@hippo91 hippo91 left a comment

Choose a reason for hiding this comment

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

@JulienPalard thanks for this clever work! 🎆

@hippo91
Copy link
Contributor

hippo91 commented Nov 21, 2020

Thanks and congrats for your first contribution! Hope to see many more!

@hippo91 hippo91 merged commit 5b4e9a0 into pylint-dev:master Nov 21, 2020
@hippo91
Copy link
Contributor

hippo91 commented Nov 21, 2020

Hi folks! When can we expect the next release including this fix?

The next pylint version will be available as soon as python3.9 will be supported. There is still works to do including into astroid.

@JulienPalard JulienPalard deleted the issue3882 branch November 22, 2020 09:28
l0b0 added a commit to linz/geostore that referenced this pull request Dec 3, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 3, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 3, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 3, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 4, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 4, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 6, 2020
l0b0 added a commit to linz/geostore that referenced this pull request Dec 6, 2020
* feat: Limit Python version to less than 3.9

Pylint does not yet support 3.9
<pylint-dev/pylint#3890 (comment)>.

* feat: Set pyenv Python version to 3.8.6

This is the most recent working version until further notice. This file
should ideally be kept in sync with the version in pyproject.toml.
LinuxMercedes added a commit to redkyn/assigner that referenced this pull request Dec 28, 2020
Should only be necessary until pylint releases their next version
See pylint-dev/pylint#3882 and pylint-dev/pylint#3890
LinuxMercedes added a commit to redkyn/assigner that referenced this pull request Dec 28, 2020
Should only be necessary until pylint releases their next version
See pylint-dev/pylint#3882 and pylint-dev/pylint#3890
bbc2 added a commit to bbc2/cryptopals that referenced this pull request Dec 29, 2020
The configuration change works around
pylint-dev/pylint#3890 which is merged but not
released yet.
bbc2 added a commit to bbc2/cryptopals that referenced this pull request Dec 29, 2020
The configuration change works around
pylint-dev/pylint#3890 which is merged but not
released yet.
Segelzwerg pushed a commit to Segelzwerg/FamilyFoto that referenced this pull request Jan 1, 2021
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.

[Python 3.9] Value 'Optional' is unsubscriptable (unsubscriptable-object) (also Union)
8 participants