[Cython] [PATCH] Add a pass transforming Python-style properties in cdef class into Cython-style properties.

Emmanuel Gil Peyrot linkmauve at linkmauve.fr
Tue Nov 3 01:37:50 EST 2015


This makes properties work at all, in cdef classes, and gives them
almost the same features as the “property something:” blocks.  The only
missing feature being the ability to assignate a docstring to the
property itself, not only to the getter, setter and deleter.

Fixes T264.
---
 Cython/Compiler/ParseTreeTransforms.py           | 72 ++++++++++++++++++++++++
 Cython/Compiler/Pipeline.py                      |  3 +-
 tests/run/cdef_class_property_decorator_T264.pyx | 51 +++++++++++++++++
 3 files changed, 125 insertions(+), 1 deletion(-)
 create mode 100644 tests/run/cdef_class_property_decorator_T264.pyx

diff --git a/Cython/Compiler/ParseTreeTransforms.py b/Cython/Compiler/ParseTreeTransforms.py
index b34f44b..67507ce 100644
--- a/Cython/Compiler/ParseTreeTransforms.py
+++ b/Cython/Compiler/ParseTreeTransforms.py
@@ -1270,6 +1270,78 @@ class WithTransform(CythonTransform, SkipDeclarations):
         return node
 
 
+class PropertyTransform(ScopeTrackingTransform):
+    """Originally, this was the only place where decorators were
+    transformed into the corresponding calling code.  Now, this is
+    done directly in DefNode and PyClassDefNode to avoid reassignments
+    to the function/class name - except for cdef class methods.  For
+    those, the reassignment is required as methods are originally
+    defined in the PyMethodDef struct.
+
+    The IndirectionNode allows DefNode to override the decorator
+    """
+
+    properties = {}
+
+    def visit_DefNode(self, func_node):
+        func_node = self.visit_FuncDefNode(func_node)
+        if self.scope_type != 'cclass' or not func_node.decorators:
+            return func_node
+        return self.handle_decorators(func_node, func_node.decorators,
+                                      func_node.name)
+
+    def handle_decorators(self, node, decorators, name):
+        properties = self.properties.setdefault(self.scope_node, {})
+        for decorator in decorators[::-1]:
+            decorator_node = decorator.decorator
+            if (isinstance(decorator_node, ExprNodes.NameNode) and
+                    decorator_node.name == 'property'):
+                node.name = '__get__'
+                node.decorators.remove(decorator)
+                stat_list = [node]
+                if name in properties:
+                    property = properties[name]
+                    property.body.stats = stat_list
+                    return []
+                else:
+                    property = Nodes.PropertyNode(node.pos, name=name)
+                    property.doc = EncodedString(u'')
+                    property.body = Nodes.StatListNode(node.pos, stats=stat_list)
+                    properties[name] = property
+                    return [property]
+            elif (isinstance(decorator_node, ExprNodes.AttributeNode) and
+                    decorator_node.attribute == 'setter' and
+                    decorator_node.obj.name in properties):
+                assert decorator_node.obj.name == name
+                node.name = '__set__'
+                node.decorators.remove(decorator)
+                property = properties[name]
+                stats = property.body.stats
+                for i, stat in enumerate(stats):
+                    if stat.name == '__set__':
+                        stats[i] = node
+                        break
+                else:
+                    stats.append(node)
+                return []
+            elif (isinstance(decorator_node, ExprNodes.AttributeNode) and
+                    decorator_node.attribute == 'deleter' and
+                    decorator_node.obj.name in properties):
+                assert decorator_node.obj.name == name
+                node.name = '__del__'
+                node.decorators.remove(decorator)
+                property = properties[name]
+                stats = property.body.stats
+                for i, stat in enumerate(stats):
+                    if stat.name == '__del__':
+                        stats[i] = node
+                        break
+                else:
+                    stats.append(node)
+                return []
+        return node
+
+
 class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations):
     """Originally, this was the only place where decorators were
     transformed into the corresponding calling code.  Now, this is
diff --git a/Cython/Compiler/Pipeline.py b/Cython/Compiler/Pipeline.py
index 833e22c..0506aff 100644
--- a/Cython/Compiler/Pipeline.py
+++ b/Cython/Compiler/Pipeline.py
@@ -171,7 +171,7 @@ def create_pipeline(context, mode, exclude_classes=()):
     from .ParseTreeTransforms import CreateClosureClasses, MarkClosureVisitor, DecoratorTransform
     from .ParseTreeTransforms import InterpretCompilerDirectives, TransformBuiltinMethods
     from .ParseTreeTransforms import ExpandInplaceOperators, ParallelRangeTransform
-    from .ParseTreeTransforms import CalculateQualifiedNamesTransform
+    from .ParseTreeTransforms import CalculateQualifiedNamesTransform, PropertyTransform
     from .TypeInference import MarkParallelAssignments, MarkOverflowingArithmetic
     from .ParseTreeTransforms import AdjustDefByDirectives, AlignFunctionDefinitions
     from .ParseTreeTransforms import RemoveUnreachableCode, GilCheck
@@ -216,6 +216,7 @@ def create_pipeline(context, mode, exclude_classes=()):
         RemoveUnreachableCode(context),
         ConstantFolding(),
         FlattenInListTransform(),
+        PropertyTransform(context),
         DecoratorTransform(context),
         ForwardDeclareTypes(context),
         AnalyseDeclarationsTransform(context),
diff --git a/tests/run/cdef_class_property_decorator_T264.pyx b/tests/run/cdef_class_property_decorator_T264.pyx
new file mode 100644
index 0000000..94b862c
--- /dev/null
+++ b/tests/run/cdef_class_property_decorator_T264.pyx
@@ -0,0 +1,51 @@
+# mode: run
+# ticket: 264
+# tag: property, decorator
+
+cdef class Prop(object):
+    """
+    >>> p = Prop()
+    >>> p.prop
+    GETTING 'None'
+    >>> p.prop = 1
+    SETTING '1' (previously: 'None')
+    >>> p.prop
+    GETTING '1'
+    1
+    >>> p.prop = 2
+    SETTING '2' (previously: '1')
+    >>> p.prop
+    GETTING '2'
+    2
+    >>> del p.prop
+    DELETING '2'
+    >>> p.prop
+    GETTING 'None'
+    """
+    cdef _value
+    def __init__(self):
+        self._value = None
+
+    @property
+    def prop(self):
+        print("FAIL")
+        return 0
+
+    @prop.deleter
+    def prop(self):
+        print("FAIL")
+
+    @property
+    def prop(self):
+        print("GETTING '%s'" % self._value)
+        return self._value
+
+    @prop.setter
+    def prop(self, value):
+        print("SETTING '%s' (previously: '%s')" % (value, self._value))
+        self._value = value
+
+    @prop.deleter
+    def prop(self):
+        print("DELETING '%s'" % self._value)
+        self._value = None
-- 
2.6.2


More information about the cython-devel mailing list