Skip to content

Fix regression property no member 2641 #890

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

4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ What's New in astroid 2.5.0?
============================
Release Date: TBA

* Adds `attr_fset` in the `PropertyModel` class.

Fixes PyCQA/pylint#3480

* Remove support for Python 3.5.
* Remove the runtime dependency on ``six``. The ``six`` brain remains in
astroid.
Expand Down
54 changes: 45 additions & 9 deletions astroid/interpreter/objectmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@
import os
import types
from functools import lru_cache
from typing import Optional

import astroid
from astroid import context as contextmod
from astroid import exceptions
from astroid import node_classes
from astroid import util

# Prevents circular imports
objects = util.lazy_import("objects")


IMPL_PREFIX = "attr_"
Expand Down Expand Up @@ -715,9 +720,6 @@ def attr_items(self):
elems.append(elem)
obj.postinit(elts=elems)

# pylint: disable=import-outside-toplevel; circular import
from astroid import objects

obj = objects.DictItems(obj)
return self._generic_dict_attribute(obj, "items")

Expand All @@ -727,9 +729,6 @@ def attr_keys(self):
obj = node_classes.List(parent=self._instance)
obj.postinit(elts=keys)

# pylint: disable=import-outside-toplevel; circular import
from astroid import objects

obj = objects.DictKeys(obj)
return self._generic_dict_attribute(obj, "keys")

Expand All @@ -740,9 +739,6 @@ def attr_values(self):
obj = node_classes.List(parent=self._instance)
obj.postinit(values)

# pylint: disable=import-outside-toplevel; circular import
from astroid import objects

obj = objects.DictValues(obj)
return self._generic_dict_attribute(obj, "values")

Expand Down Expand Up @@ -794,6 +790,46 @@ def infer_call_result(self, caller=None, context=None):
property_accessor.postinit(args=func.args, body=func.body)
return property_accessor

@property
def attr_fset(self):
from astroid.scoped_nodes import FunctionDef

func = self._instance

def find_setter(func: objects.Property) -> Optional[astroid.FunctionDef]:
"""
Given a property, find the corresponding setter function and returns it.

:param func: property for which the setter has to be found
:return: the setter function or None
"""
for target in [
t for t in func.parent.get_children() if t.name == func.function.name
]:
for dec_name in target.decoratornames():
if dec_name.endswith(func.function.name + ".setter"):
return target
return None

func_setter = find_setter(func)
if not func_setter:
raise exceptions.InferenceError(
f"Unable to find the setter of property {func.function.name}"
)

class PropertyFuncAccessor(FunctionDef):
def infer_call_result(self, caller=None, context=None):
nonlocal func_setter
if caller and len(caller.args) != 2:
raise exceptions.InferenceError(
"fset() needs two arguments", target=self, context=context
)
yield from func_setter.infer_call_result(caller=caller, context=context)

property_accessor = PropertyFuncAccessor(name="fset", parent=self._instance)
property_accessor.postinit(args=func_setter.args, body=func_setter.body)
return property_accessor

@property
def attr_setter(self):
return self._init_function("setter")
Expand Down
11 changes: 10 additions & 1 deletion tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -5494,14 +5494,19 @@ class A:
def test(self):
return 42

@test.setter
def test(self, value):
return "banco"

A.test #@
A().test #@
A.test.fget(A) #@
A.test.fset(A, "a_value") #@
A.test.setter #@
A.test.getter #@
A.test.deleter #@
"""
prop, prop_result, prop_fget_result, prop_setter, prop_getter, prop_deleter = extract_node(
prop, prop_result, prop_fget_result, prop_fset_result, prop_setter, prop_getter, prop_deleter = extract_node(
code
)

Expand All @@ -5519,6 +5524,10 @@ def test(self):
assert isinstance(inferred, nodes.Const)
assert inferred.value == 42

inferred = next(prop_fset_result.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value == "banco"

for prop_func in prop_setter, prop_getter, prop_deleter:
inferred = next(prop_func.infer())
assert isinstance(inferred, nodes.FunctionDef)
Expand Down