summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog4
-rw-r--r--astroid/brain/brain_attrs.py40
-rw-r--r--astroid/tests/unittest_brain.py26
-rw-r--r--tox.ini1
4 files changed, 71 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index e2a2d4eb..16a9bf46 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,6 +2,10 @@ Change log for the astroid package (used to be astng)
=====================================================
--
+ * Add brain tip for attrs library to prevent unsupported-assignment-operation false positives
+
+ Close PYCQA/pylint#1698
+
* file_stream was removed, since it was deprecated for three releases
Instead one should use the .stream() method.
diff --git a/astroid/brain/brain_attrs.py b/astroid/brain/brain_attrs.py
new file mode 100644
index 00000000..a60d52ed
--- /dev/null
+++ b/astroid/brain/brain_attrs.py
@@ -0,0 +1,40 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/master/COPYING.LESSER
+"""
+Astroid hook for the attrs library
+
+Without this hook pylint reports unsupported-assignment-operation
+for atrrs classes
+"""
+
+import astroid
+from astroid import MANAGER
+
+
+def is_decorated_with_attrs(
+ node, decorator_names=('attr.s', 'attr.attrs', 'attr.attributes')):
+ """Return True if a decorated node has
+ an attr decorator applied."""
+ if not node.decorators:
+ return False
+ for decorator_attribute in node.decorators.nodes:
+ if decorator_attribute.as_string() in decorator_names:
+ return True
+ return False
+
+
+def attr_attributes_transform(node):
+ """Given that the ClassNode has an attr decorator,
+ rewrite class attributes as instance attributes
+ """
+ for cdefbodynode in node.body:
+ if not isinstance(cdefbodynode, astroid.Assign):
+ continue
+ for target in cdefbodynode.targets:
+ node.locals[target.name] = [astroid.Attribute(target.name)]
+
+
+MANAGER.register_transform(
+ astroid.Class,
+ attr_attributes_transform,
+ is_decorated_with_attrs)
diff --git a/astroid/tests/unittest_brain.py b/astroid/tests/unittest_brain.py
index 229ab11a..be4d175b 100644
--- a/astroid/tests/unittest_brain.py
+++ b/astroid/tests/unittest_brain.py
@@ -44,6 +44,13 @@ try:
HAS_PYTEST = True
except ImportError:
HAS_PYTEST = False
+
+try:
+ import attr as attr_module # pylint: disable=unused-import
+ HAS_ATTR = True
+except ImportError:
+ HAS_ATTR = False
+
import six
from astroid import MANAGER
@@ -799,5 +806,24 @@ class BrainUUIDTest(unittest.TestCase):
self.assertIsInstance(inferred, nodes.Const)
+@unittest.skipUnless(HAS_ATTR, "These tests require the attr library")
+class AttrsTest(unittest.TestCase):
+ def test_attr_transform(self):
+ module = astroid.parse("""
+ import attr
+
+ @attr.s
+ class Foo:
+
+ d = attr.ib(attr.Factory(dict))
+
+ f = Foo()
+ f.d['answer'] = 42
+ """)
+
+ should_be_attribute = next(module.getattr('f')[0].infer()).getattr('d')[0]
+ self.assertIsInstance(should_be_attribute, astroid.Attribute)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/tox.ini b/tox.ini
index 7ed9d36a..d0f35fbb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,6 +12,7 @@ deps =
lazy-object-proxy
nose
py27,py34,py35,py36: numpy
+ py27,py34,py35,py36: attr
pytest
python-dateutil
py27,py33,pypy: singledispatch