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

Fix strong references to mutable objects in context.clone #927

Merged

Conversation

nelfin
Copy link
Contributor

@nelfin nelfin commented Mar 29, 2021

Steps

  • For new features or bug fixes, add a ChangeLog entry describing what your PR does.
  • Write a good description on what the PR does.

Description

Since InferenceContext.clone passes self.path when
constructing a new instance, these two objects will hold references to
the same underlying set object. This caused modifications to
context.path further down in the stack to be reflected in "earlier"
objects and this would incorrectly detect a loop if the same node was
used twice in the inference of a some node (instead of only if an
inference result was used in inferring itself).

Type of Changes

Type
🐛 Bug fix
✨ New feature
🔨 Refactoring
📜 Docs

Related Issue

Closes #926

@@ -1703,7 +1703,8 @@ def __init__(self):
"""
ast = extract_node(code, __name__)
expr = ast.func.expr
self.assertIs(next(expr.infer()), util.Uninferable)
with pytest.raises(exceptions.InferenceError):
next(expr.infer())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test has changed behaviour. I assume that it's okay for it to raise InferenceError instead of returning Uninferable as long as it still doesn't leak StopIteration

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems fair.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cdce8p what is your opinion?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not too familiar with that part of the code base. If the pylint tests continue to pass, I would guess it's fine.

Copy link
Member

Choose a reason for hiding this comment

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

Potentially this could cause new crash if an inference error is raised in calling code ? I don't think pylint tests cover all possible edge case.

@nelfin nelfin force-pushed the fix/926-strong-mutable-ref-in-context-clone branch from b09297c to 573e018 Compare March 29, 2021 13:14
@nelfin
Copy link
Contributor Author

nelfin commented Mar 29, 2021

Updated and rebased to appease code formatting overlords

@nelfin nelfin force-pushed the fix/926-strong-mutable-ref-in-context-clone branch from 573e018 to 04822ab Compare April 7, 2021 23:50
@hippo91 hippo91 self-assigned this Apr 8, 2021
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.

@nelfin it is a great improvment. In fact you succeeded where i failed with #884. Congrats!
I just left a bunch of comments.
I also tried the tests of pylint with your astroid branch and one test is failing. It deals with logging module and it is possible that the warnings that are now emitted are correct. Could you please have a look to it? @Pierre-Sassoulas can you also have a look? I saw you just reintroduced the concerned lines, so you will be much pertinent than me.

tests/unittest_inference.py Show resolved Hide resolved
tests/unittest_inference.py Show resolved Hide resolved
tests/unittest_inference.py Outdated Show resolved Hide resolved
tests/unittest_inference.py Outdated Show resolved Hide resolved
@@ -1703,7 +1703,8 @@ def __init__(self):
"""
ast = extract_node(code, __name__)
expr = ast.func.expr
self.assertIs(next(expr.infer()), util.Uninferable)
with pytest.raises(exceptions.InferenceError):
next(expr.infer())
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems fair.

tests/unittest_regrtest.py Show resolved Hide resolved
@nelfin nelfin force-pushed the fix/926-strong-mutable-ref-in-context-clone branch from 04822ab to c30fd42 Compare April 8, 2021 23:00
@nelfin
Copy link
Contributor Author

nelfin commented Apr 8, 2021

I also tried the tests of pylint with your astroid branch and one test is failing. It deals with logging module and it is possible that the warnings that are now emitted are correct. Could you please have a look to it?

I think the new messages are okay? There wasn't any warning previously raised for the following:

class Logger(renamed_logging.Logger):
    pass

custom_logger = Logger('three')
custom_logger.info('testing {0}'.format('info'))  # here
custom_logger.info('testing %s' % 'info')   # and here

but those newly raised warning match the behaviour you would expect if Logger was being correctly inferred now. I'll update that test and submit a PR to pylint

@hippo91
Copy link
Contributor

hippo91 commented Apr 9, 2021

@nelfin i will wait for @Pierre-Sassoulas remarks concerning the pylint test before merging this. Thanks again.

@nelfin
Copy link
Contributor Author

nelfin commented Apr 9, 2021

@nelfin i will wait for @Pierre-Sassoulas remarks concerning the pylint test before merging this.

No problem.

Thanks again.

Thank you!

@cdce8p
Copy link
Member

cdce8p commented Apr 9, 2021

@hippo91 I just looked at pylint-dev/pylint#4325 and also ran the other functional tests. The error with the logging tests is indeed a false-negative currently. Since all seems to work as expected then, I would say this is good to go.

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.

👍

Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

@Pierre-Sassoulas mentioned here that the change from Uninferable to StopIteration might have unintended consequences. After I saw it, I ran the changes against one of my repos and indeed saw multiple errors. Namely: not-callable, unsupported-membership-test, and invalid-sequence-index. I haven't investigated what code exactly is causing these, but it's safe to assume this shouldn't be merged just yet.

The Github Actions run: https://github.com/cdce8p/ha-core/runs/2308362444?check_suite_focus=true#step:7:58

@cdce8p
Copy link
Member

cdce8p commented Apr 10, 2021

I had some time to look at two of them and reduced it down to code snippets. Maybe that helps.

unsupported-membership-test

# pylint: disable=missing-docstring,too-few-public-methods,invalid-name
from enum import Enum

class MyEnum(Enum):
    CONST1 = "const"

def name_in_enum(name):
    # false-positive: unsupported-membership-test
    if name in MyEnum.__members__:
        return

not-callable

pip install aiohttp python-telnet-vlc
# pylint: disable=missing-docstring,unused-import,protected-access
from aiohttp import web
from python_telnet_vlc import VLCTelnet

def func():
    app = web.Application()
    app._router.freeze = lambda: None

class VlcDevice:
    def __init__(self):
        self._vlc = VLCTelnet("VLC-TELNET", "pwd", "0000")

    def media_previous_track(self):
        # False-positive: not-callable
        self._vlc.prev()

    def media_next_track_abc(self):
        # False-positive: not-callable
        self._vlc.next()

@nelfin
Copy link
Contributor Author

nelfin commented Apr 10, 2021

I had some time to look at two of them and reduced it down to code snippets. Maybe that helps.

Thanks for that, it helps a lot.

I spent some time trying to replicate these messages from your bulid log and debugging the causes. I picked up these errors from the log in the link:

Error: homeassistant/components/vlc_telnet/media_player.py:289:8: E1102: self._vlc.prev is not callable (not-callable)
Error: homeassistant/components/vlc_telnet/media_player.py:293:8: E1102: self._vlc.next is not callable (not-callable)
Error: homeassistant/components/mpd/media_player.py:393:14: E1102: self._client.next is not callable (not-callable)
Error: homeassistant/components/ezviz/binary_sensor.py:26:23: E1135: Value 'BinarySensorType.__members__' doesn't support membership test (unsupported-membership-test)
Error: homeassistant/components/ezviz/sensor.py:26:23: E1135: Value 'SensorType.__members__' doesn't support membership test (unsupported-membership-test)
Error: homeassistant/components/xmpp/notify.py:193:20: E1126: Sequence index is not an int, slice, or instance with __index__ (invalid-sequence-index)

The not-callable messages stem from the fact that the previous behaviour was to infer [<Const.None>, <BoundMethod.add>] but after the application of this change it only infers [Const.None]. I haven't yet determined the cause and hopefully a smaller snippet will help.

The Enum.__members__ seems to be a false-positive. I'll see why, given that it should probably be inferred as [''] via brain_namedtuple_enum. I reduced the example to an almost identical snippet to yours.

The invalid-sequence-index from xmpp/notify.py:193 is interesting. It is newly inferred, the type of message was not inferred before. Inferring message gets us an instance of xmpp.Message, but inferring the Subscript gets us a list since bases.Instance.getitem returns the first inferred result from infer_call_result on __getitem__. In the general case, we should probably update infer_getitem to return all values so that pylint/safe_infer doesn't return (maybe extend _check_invalid_sequence_index to check that all inferred values are sequence types, like how no-member only emits if none of the inferred values have that attribute). In the specific case of xmpp.Message/ElementBase, there's potential for a brain to be added to return Uninferable because the behaviour of __getitem__ can change dynamically at runtime due to loaded plugins. Here is a minimal example that matches the behaviour that I consider to be a false-positive:

class DynamicGetitem:
    def __getitem__(self, key):
        if key == 'attributes':
            return []
        return {'world': 123}


ex = DynamicGetitem()
ex['hello']['world']
#  E1126: Sequence index is not an int, slice, or instance with __index__ (invalid-sequence-index)

@nelfin
Copy link
Contributor Author

nelfin commented Apr 11, 2021

The Enum.__members__ seems to be a false-positive. I'll see why, given that it should probably be inferred as [''] via brain_namedtuple_enum. I reduced the example to an almost identical snippet to yours.

This wasn't quite correct. members = [''] is for enums built via Enum.__call__, e.g. Foo = Enum('Foo', 'bar baz').

So it looks like this has uncovered an existing bug: before the patch the value of SensorType.__members__ was Uninferable so the membership test check was skipped, after the patch __members__ is inferred to a Property and not transformed to the expected value of that property. Here's an example repro that fails with pylint 2.7.4 on current astroid master f2b197a:

# enum_repro.py

class BaseMeta(type):
    def __new__(metacls, cls, bases, classdict):
        classdict['__members__'] = metacls.__members__
        return super().__new__(metacls, cls, bases, classdict)

    @property
    def __members__(cls):
        return {'hello', 'world'}


class Parent(metaclass=BaseMeta):
    pass


class Derived(Parent):
    pass


assert 'hello' in Parent.__members__
assert 'world' in Derived.__members__

And example output from astroid:

# enum_repro.py

import astroid
mod = astroid.MANAGER.ast_from_file('enum_repro_mod.py', 'mod', source=True)
Parent, Derived = mod.body[1], mod.body[2]
print(list(Parent.igetattr('__members__')))
# [<Set.set l.10 at 0x...>]
print(list(Derived.igetattr('__members__')))
# [<Property.__members__ l.8 at 0x...>]

nelfin added a commit to nelfin/astroid that referenced this pull request Apr 11, 2021
Ref pylint-dev#927. `metaclass` tests seem related, `property` tests possibly
related. pylint-dev#927 (comment)
@nelfin
Copy link
Contributor Author

nelfin commented Apr 13, 2021

@cdce8p: aiohttp and VLCTelnet turned out to be red herrings. This case fails on current stable versions:

class Example:
    def prev(self):
        pass
    def next(self):
        pass
    def other(self):
        pass


ex = Example()
ex.other()  # no warning
ex.prev()   # no warning
ex.next()   # no warning

import typing

ex.other()  # no warning
ex.prev()   # false-positive: not-callable
ex.next()   # false-positive: not-callable

@nelfin
Copy link
Contributor Author

nelfin commented Apr 13, 2021

I've bisected this down to 78d5537. Pylint 2.3.1 passes this case with 20a7ae5 and fails with 78d5537

nelfin added 5 commits May 12, 2021 09:12
Ref pylint-dev#926. Since InferenceContext.clone passes self.path when
constructing a new instance, these two objects will hold references to
the same underlying set object. This caused modifications to
context.path further down in the stack to be reflected in "earlier"
objects and this would incorrectly detect a loop if the same node was
used twice in the inference of a some node (instead of only if an
inference result was used in inferring itself). As an illustration:

[stmt1 = something.one, stmt1 = something.two]
-> infer stmt1       <-+
-> -> infer something -+  writes back to parent context.path -+
-> <- <returns>                                               |
-> infer stmt2           affecting this context.path        <-+
-> -> infer something -> sees something in context.path,
                         decorators.path_wrapper returns None
Now raises InferenceError instead of returning Uninferable. Still no
StopIteration leak.
This reverts commit b699ebb.

The fixes to context.path also appears to stop returning Uninferable for
numpy ufuncs.
@nelfin nelfin force-pushed the fix/926-strong-mutable-ref-in-context-clone branch from c30fd42 to 0e5cd49 Compare May 11, 2021 23:15
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

Ok, let's merge it

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

Successfully merging this pull request may close these issues.

infer_stmts cannot infer multiple uses of the same AssignName
4 participants