diff options
author | scoder <stefan_ml@behnel.de> | 2020-05-04 22:54:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-04 22:54:53 +0200 |
commit | 22a49f403b6f00f1674664e01ee0f8212c06ef13 (patch) | |
tree | 9acc9b0eda3ff58845ce770ded7a33a91ef9e4af | |
parent | 3bd6ccb011e227090560286abb1990a12a51d4d5 (diff) | |
download | cython-22a49f403b6f00f1674664e01ee0f8212c06ef13.tar.gz |
Rewrite the C property feature (GH-3571)
* Rewrite C property support (GH-2640) based on inline C methods.
Supersedes GH-2640 and GH-3095.
Closes GH-3521.
* Test fix for `numpy_parallel.pyx`: avoid depending on whether "nd.shape" requires the GIL or not.
* Turn NumPy's "ndarray.data" into a property to avoid direct struct access.
* Make "ndarray.size" accessible without the GIL.
-rw-r--r-- | Cython/Compiler/ExprNodes.py | 53 | ||||
-rw-r--r-- | Cython/Compiler/Nodes.py | 97 | ||||
-rw-r--r-- | Cython/Compiler/ParseTreeTransforms.py | 131 | ||||
-rw-r--r-- | Cython/Compiler/Pipeline.py | 3 | ||||
-rw-r--r-- | Cython/Compiler/PyrexTypes.py | 20 | ||||
-rw-r--r-- | Cython/Compiler/Symtab.py | 42 | ||||
-rw-r--r-- | Cython/Includes/numpy/__init__.pxd | 31 | ||||
-rw-r--r-- | tests/run/ext_attr_getter.srctree | 145 | ||||
-rw-r--r-- | tests/run/numpy_parallel.pyx | 8 |
9 files changed, 370 insertions, 160 deletions
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py index c8c33843e..895ff3fe2 100644 --- a/Cython/Compiler/ExprNodes.py +++ b/Cython/Compiler/ExprNodes.py @@ -4024,12 +4024,9 @@ class IndexNode(_IndexingBaseNode): else: assert False, "unexpected base type in indexing: %s" % self.base.type elif self.base.type.is_cfunction: - if self.base.entry.is_cgetter: - index_code = "(%s[%s])" - else: - return "%s<%s>" % ( - self.base.result(), - ",".join([param.empty_declaration_code() for param in self.type_indices])) + return "%s<%s>" % ( + self.base.result(), + ",".join([param.empty_declaration_code() for param in self.type_indices])) elif self.base.type.is_ctuple: index = self.index.constant_result if index < 0: @@ -5594,6 +5591,16 @@ class SimpleCallNode(CallNode): except Exception as e: self.compile_time_value_error(e) + @classmethod + def for_cproperty(cls, pos, obj, entry): + # Create a call node for C property access. + property_scope = entry.scope + getter_entry = property_scope.lookup_here(entry.name) + assert getter_entry, "Getter not found in scope %s: %s" % (property_scope, property_scope.entries) + function = NameNode(pos, name=entry.name, entry=getter_entry, type=getter_entry.type) + node = cls(pos, function=function, args=[obj]) + return node + def analyse_as_type(self, env): attr = self.function.as_cython_attribute() if attr == 'pointer': @@ -6789,7 +6796,7 @@ class AttributeNode(ExprNode): is_attribute = 1 subexprs = ['obj'] - _type = PyrexTypes.error_type + type = PyrexTypes.error_type entry = None is_called = 0 needs_none_check = True @@ -6797,20 +6804,6 @@ class AttributeNode(ExprNode): is_special_lookup = False is_py_attr = 0 - @property - def type(self): - if self._type.is_cfunction and hasattr(self._type, 'entry') and self._type.entry.is_cgetter: - return self._type.return_type - return self._type - - @type.setter - def type(self, value): - # XXX review where the attribute is set - # make sure it is not already a cgetter - if self._type.is_cfunction and hasattr(self._type, 'entry') and self._type.entry.is_cgetter: - error(self.pos, "%s.type already set" % self.__name__) - self._type = value - def as_cython_attribute(self): if (isinstance(self.obj, NameNode) and self.obj.is_cython_module and not @@ -6896,7 +6889,7 @@ class AttributeNode(ExprNode): if node is None: node = self.analyse_as_ordinary_attribute_node(env, target) assert node is not None - if node.entry: + if (node.is_attribute or node.is_name) and node.entry: node.entry.used = True if node.is_attribute: node.wrap_obj_in_nonecheck(env) @@ -7029,6 +7022,11 @@ class AttributeNode(ExprNode): self.result_ctype = py_object_type elif target and self.obj.type.is_builtin_type: error(self.pos, "Assignment to an immutable object field") + elif self.entry and self.entry.is_cproperty: + if not target: + return SimpleCallNode.for_cproperty(self.pos, self.obj, self.entry).analyse_types(env) + # TODO: implement writable C-properties? + error(self.pos, "Assignment to a read-only property") #elif self.type.is_memoryviewslice and not target: # self.is_temp = True return self @@ -7085,8 +7083,11 @@ class AttributeNode(ExprNode): # fused function go through assignment synthesis # (foo = pycfunction(foo_func_obj)) and need to go through # regular Python lookup as well - if (entry.is_variable and not entry.fused_cfunction) or entry.is_cmethod: - self._type = entry.type + if entry.is_cproperty: + self.type = entry.type + return + elif (entry.is_variable and not entry.fused_cfunction) or entry.is_cmethod: + self.type = entry.type self.member = entry.cname return else: @@ -7106,7 +7107,7 @@ class AttributeNode(ExprNode): # mangle private '__*' Python attributes used inside of a class self.attribute = env.mangle_class_private_name(self.attribute) self.member = self.attribute - self._type = py_object_type + self.type = py_object_type self.is_py_attr = 1 if not obj_type.is_pyobject and not obj_type.is_error: @@ -7188,8 +7189,6 @@ class AttributeNode(ExprNode): obj_code = obj.result_as(obj.type) #print "...obj_code =", obj_code ### if self.entry and self.entry.is_cmethod: - if self.entry.is_cgetter: - return "%s(%s)" % (self.entry.func_cname, obj_code) if obj.type.is_extension_type and not self.entry.is_builtin_cmethod: if self.entry.final_func_cname: return self.entry.final_func_cname diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py index aa6741bff..5af5323c3 100644 --- a/Cython/Compiler/Nodes.py +++ b/Cython/Compiler/Nodes.py @@ -21,7 +21,7 @@ from . import Naming from . import PyrexTypes from . import TypeSlots from .PyrexTypes import py_object_type, error_type -from .Symtab import (ModuleScope, LocalScope, ClosureScope, +from .Symtab import (ModuleScope, LocalScope, ClosureScope, PropertyScope, StructOrUnionScope, PyClassScope, CppClassScope, TemplateScope, punycodify_name) from .Code import UtilityCode @@ -154,6 +154,7 @@ class Node(object): is_literal = 0 is_terminator = 0 is_wrapper = False # is a DefNode wrapper for a C function + is_cproperty = False temps = None # All descendants should set child_attrs to a list of the attributes @@ -453,8 +454,13 @@ class CDefExternNode(StatNode): env.add_include_file(self.include_file, self.verbatim_include, late) def analyse_expressions(self, env): + # Allow C properties, inline methods, etc. also in external types. + self.body = self.body.analyse_expressions(env) return self + def generate_function_definitions(self, env, code): + self.body.generate_function_definitions(env, code) + def generate_execution_code(self, code): pass @@ -479,6 +485,9 @@ class CDeclaratorNode(Node): calling_convention = "" + def declared_name(self): + return None + def analyse_templates(self): # Only C++ functions have templates. return None @@ -493,6 +502,9 @@ class CNameDeclaratorNode(CDeclaratorNode): default = None + def declared_name(self): + return self.name + def analyse(self, base_type, env, nonempty=0, visibility=None, in_pxd=False): if nonempty and self.name == '': # May have mistaken the name for the type. @@ -516,6 +528,9 @@ class CPtrDeclaratorNode(CDeclaratorNode): child_attrs = ["base"] + def declared_name(self): + return self.base.declared_name() + def analyse_templates(self): return self.base.analyse_templates() @@ -531,6 +546,9 @@ class CReferenceDeclaratorNode(CDeclaratorNode): child_attrs = ["base"] + def declared_name(self): + return self.base.declared_name() + def analyse_templates(self): return self.base.analyse_templates() @@ -603,6 +621,9 @@ class CFuncDeclaratorNode(CDeclaratorNode): is_const_method = 0 templates = None + def declared_name(self): + return self.base.declared_name() + def analyse_templates(self): if isinstance(self.base, CArrayDeclaratorNode): from .ExprNodes import TupleNode, NameNode @@ -836,6 +857,9 @@ class CArgDeclNode(Node): annotation = None is_dynamic = 0 + def declared_name(self): + return self.declarator.declared_name() + @property def name_cstring(self): return self.name.as_c_string_literal() @@ -1728,10 +1752,6 @@ class FuncDefNode(StatNode, BlockNode): def generate_function_definitions(self, env, code): from . import Buffer - if self.entry.is_cgetter: - # no code to generate - return - lenv = self.local_scope if lenv.is_closure_scope and not lenv.is_passthrough: outer_scope_cname = "%s->%s" % (Naming.cur_scope_cname, @@ -2344,7 +2364,7 @@ class CFuncDefNode(FuncDefNode): # is_static_method whether this is a static method # is_c_class_method whether this is a cclass method - child_attrs = ["base_type", "declarator", "body", "py_func_stat", "decorators"] + child_attrs = ["base_type", "declarator", "body", "decorators", "py_func_stat"] outer_attrs = ["decorators", "py_func_stat"] inline_in_pxd = False @@ -2359,27 +2379,15 @@ class CFuncDefNode(FuncDefNode): def unqualified_name(self): return self.entry.name + def declared_name(self): + return self.declarator.declared_name() + @property def code_object(self): # share the CodeObject with the cpdef wrapper (if available) return self.py_func.code_object if self.py_func else None def analyse_declarations(self, env): - is_property = 0 - if self.decorators: - for decorator in self.decorators: - func = decorator.decorator - if func.is_name: - if func.name == 'property': - is_property = 1 - elif func.name == 'staticmethod': - pass - else: - error(self.pos, "Cannot handle %s decorators yet" % func.name) - else: - error(self.pos, - "Cannot handle %s decorators yet" % type(func).__name__) - self.is_c_class_method = env.is_c_class_scope if self.directive_locals is None: self.directive_locals = {} @@ -2458,10 +2466,6 @@ class CFuncDefNode(FuncDefNode): cname=cname, visibility=self.visibility, api=self.api, defining=self.body is not None, modifiers=self.modifiers, overridable=self.overridable) - if is_property: - self.entry.is_property = 1 - env.property_entries.append(self.entry) - env.cfunc_entries.remove(self.entry) self.entry.inline_func_in_pxd = self.inline_in_pxd self.return_type = typ.return_type if self.return_type.is_array and self.visibility != 'extern': @@ -2628,7 +2632,7 @@ class CFuncDefNode(FuncDefNode): header = self.return_type.declaration_code(entity, dll_linkage=dll_linkage) #print (storage_class, modifiers, header) - needs_proto = self.is_c_class_method + needs_proto = self.is_c_class_method or self.entry.is_cproperty if self.template_declaration: if needs_proto: code.globalstate.parts['module_declarations'].putln(self.template_declaration) @@ -5332,14 +5336,13 @@ class PropertyNode(StatNode): # # name string # doc EncodedString or None Doc string - # entry Symtab.Entry + # entry Symtab.Entry The Entry of the property attribute # body StatListNode child_attrs = ["body"] def analyse_declarations(self, env): self.entry = env.declare_property(self.name, self.doc, self.pos) - self.entry.scope.directives = env.directives self.body.analyse_declarations(self.entry.scope) def analyse_expressions(self, env): @@ -5356,6 +5359,44 @@ class PropertyNode(StatNode): self.body.annotate(code) +class CPropertyNode(StatNode): + """Definition of a C property, backed by a CFuncDefNode getter. + """ + # name string + # doc EncodedString or None Doc string of the property + # entry Symtab.Entry The Entry of the property attribute + # body StatListNode[CFuncDefNode] (for compatibility with PropertyNode) + + child_attrs = ["body"] + is_cproperty = True + + @property + def cfunc(self): + stats = self.body.stats + assert stats and isinstance(stats[0], CFuncDefNode), stats + return stats[0] + + def analyse_declarations(self, env): + scope = PropertyScope(self.name, class_scope=env) + self.body.analyse_declarations(scope) + entry = self.entry = env.declare_property( + self.name, self.doc, self.pos, ctype=self.cfunc.return_type, property_scope=scope) + entry.getter_cname = self.cfunc.entry.cname + + def analyse_expressions(self, env): + self.body = self.body.analyse_expressions(env) + return self + + def generate_function_definitions(self, env, code): + self.body.generate_function_definitions(env, code) + + def generate_execution_code(self, code): + pass + + def annotate(self, code): + self.body.annotate(code) + + class GlobalNode(StatNode): # Global variable declaration. # diff --git a/Cython/Compiler/ParseTreeTransforms.py b/Cython/Compiler/ParseTreeTransforms.py index d722529b8..15feda18f 100644 --- a/Cython/Compiler/ParseTreeTransforms.py +++ b/Cython/Compiler/ParseTreeTransforms.py @@ -593,14 +593,12 @@ class PxdPostParse(CythonTransform, SkipDeclarations): err = self.ERR_INLINE_ONLY if (isinstance(node, Nodes.DefNode) and self.scope_type == 'cclass' - and node.name in ('__getbuffer__', '__releasebuffer__')): + and node.name in ('__getbuffer__', '__releasebuffer__')): err = None # allow these slots if isinstance(node, Nodes.CFuncDefNode): - if node.decorators and self.scope_type == 'cclass': - err = None - elif (u'inline' in node.modifiers and - self.scope_type in ('pxd', 'cclass')): + if (u'inline' in node.modifiers and + self.scope_type in ('pxd', 'cclass')): node.inline_in_pxd = True if node.visibility != 'private': err = self.ERR_NOGO_WITH_INLINE % node.visibility @@ -1365,7 +1363,7 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): if self._properties is None: self._properties = [] self._properties.append({}) - super(DecoratorTransform, self).visit_CClassDefNode(node) + node = super(DecoratorTransform, self).visit_CClassDefNode(node) self._properties.pop() return node @@ -1375,6 +1373,25 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): warning(node.pos, "'property %s:' syntax is deprecated, use '@property'" % node.name, level) return node + def visit_CFuncDefNode(self, node): + node = self.visit_FuncDefNode(node) + if self.scope_type != 'cclass' or self.scope_node.visibility != "extern" or not node.decorators: + return node + + ret_node = node + decorator_node = self._find_property_decorator(node) + if decorator_node: + if decorator_node.decorator.is_name: + name = node.declared_name() + if name: + ret_node = self._add_property(node, name, decorator_node) + else: + error(decorator_node.pos, "C property decorator can only be @property") + + if node.decorators: + return self._reject_decorated_property(node, node.decorators[0]) + return ret_node + def visit_DefNode(self, node): scope_type = self.scope_type node = self.visit_FuncDefNode(node) @@ -1382,28 +1399,12 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): return node # transform @property decorators - properties = self._properties[-1] - for decorator_node in node.decorators[::-1]: + decorator_node = self._find_property_decorator(node) + if decorator_node is not None: decorator = decorator_node.decorator - if decorator.is_name and decorator.name == 'property': - if len(node.decorators) > 1: - return self._reject_decorated_property(node, decorator_node) - name = node.name - node.name = EncodedString('__get__') - node.decorators.remove(decorator_node) - stat_list = [node] - if name in properties: - prop = properties[name] - prop.pos = node.pos - prop.doc = node.doc - prop.body.stats = stat_list - return [] - prop = Nodes.PropertyNode(node.pos, name=name) - prop.doc = node.doc - prop.body = Nodes.StatListNode(node.pos, stats=stat_list) - properties[name] = prop - return [prop] - elif decorator.is_attribute and decorator.obj.name in properties: + if decorator.is_name: + return self._add_property(node, node.name, decorator_node) + else: handler_name = self._map_property_attribute(decorator.attribute) if handler_name: if decorator.obj.name != node.name: @@ -1414,7 +1415,7 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): elif len(node.decorators) > 1: return self._reject_decorated_property(node, decorator_node) else: - return self._add_to_property(properties, node, handler_name, decorator_node) + return self._add_to_property(node, handler_name, decorator_node) # we clear node.decorators, so we need to set the # is_staticmethod/is_classmethod attributes now @@ -1429,6 +1430,18 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): node.decorators = None return self.chain_decorators(node, decs, node.name) + def _find_property_decorator(self, node): + properties = self._properties[-1] + for decorator_node in node.decorators[::-1]: + decorator = decorator_node.decorator + if decorator.is_name and decorator.name == 'property': + # @property + return decorator_node + elif decorator.is_attribute and decorator.obj.name in properties: + # @prop.setter etc. + return decorator_node + return None + @staticmethod def _reject_decorated_property(node, decorator_node): # restrict transformation to outermost decorator as wrapped properties will probably not work @@ -1437,9 +1450,42 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): error(deco.pos, "Property methods with additional decorators are not supported") return node - @staticmethod - def _add_to_property(properties, node, name, decorator): + def _add_property(self, node, name, decorator_node): + if len(node.decorators) > 1: + return self._reject_decorated_property(node, decorator_node) + node.decorators.remove(decorator_node) + properties = self._properties[-1] + is_cproperty = isinstance(node, Nodes.CFuncDefNode) + body = Nodes.StatListNode(node.pos, stats=[node]) + if is_cproperty: + if name in properties: + error(node.pos, "C property redeclared") + if 'inline' not in node.modifiers: + error(node.pos, "C property method must be declared 'inline'") + prop = Nodes.CPropertyNode(node.pos, doc=node.doc, name=name, body=body) + elif name in properties: + prop = properties[name] + if prop.is_cproperty: + error(node.pos, "C property redeclared") + else: + node.name = EncodedString("__get__") + prop.pos = node.pos + prop.doc = node.doc + prop.body.stats = [node] + return None + else: + node.name = EncodedString("__get__") + prop = Nodes.PropertyNode( + node.pos, name=name, doc=node.doc, body=body) + properties[name] = prop + return prop + + def _add_to_property(self, node, name, decorator): + properties = self._properties[-1] prop = properties[node.name] + if prop.is_cproperty: + error(node.pos, "C property redeclared") + return None node.name = name node.decorators.remove(decorator) stats = prop.body.stats @@ -1449,7 +1495,7 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations): break else: stats.append(node) - return [] + return None @staticmethod def chain_decorators(node, decorators, name): @@ -2274,29 +2320,6 @@ class AnalyseExpressionsTransform(CythonTransform): node = node.base return node -class ReplacePropertyNode(CythonTransform): - def visit_CFuncDefNode(self, node): - if not node.decorators: - return node - decorator = self.find_first_decorator(node, 'property') - if decorator: - # transform class functions into c-getters - if len(node.decorators) > 1: - # raises - self._reject_decorated_property(node, decorator_node) - node.entry.is_cgetter = True - # Add a func_cname to be output instead of the attribute - node.entry.func_cname = node.body.stats[0].value.function.name - node.decorators.remove(decorator) - return node - - def find_first_decorator(self, node, name): - for decorator_node in node.decorators[::-1]: - decorator = decorator_node.decorator - if decorator.is_name and decorator.name == name: - return decorator_node - return None - class FindInvalidUseOfFusedTypes(CythonTransform): diff --git a/Cython/Compiler/Pipeline.py b/Cython/Compiler/Pipeline.py index f4fbb6e11..5194c3e49 100644 --- a/Cython/Compiler/Pipeline.py +++ b/Cython/Compiler/Pipeline.py @@ -146,7 +146,7 @@ def create_pipeline(context, mode, exclude_classes=()): from .ParseTreeTransforms import CreateClosureClasses, MarkClosureVisitor, DecoratorTransform from .ParseTreeTransforms import TrackNumpyAttributes, InterpretCompilerDirectives, TransformBuiltinMethods from .ParseTreeTransforms import ExpandInplaceOperators, ParallelRangeTransform - from .ParseTreeTransforms import CalculateQualifiedNamesTransform, ReplacePropertyNode + from .ParseTreeTransforms import CalculateQualifiedNamesTransform from .TypeInference import MarkParallelAssignments, MarkOverflowingArithmetic from .ParseTreeTransforms import AdjustDefByDirectives, AlignFunctionDefinitions from .ParseTreeTransforms import RemoveUnreachableCode, GilCheck @@ -198,7 +198,6 @@ def create_pipeline(context, mode, exclude_classes=()): AnalyseDeclarationsTransform(context), AutoTestDictTransform(context), EmbedSignature(context), - ReplacePropertyNode(context), EarlyReplaceBuiltinCalls(context), ## Necessary? TransformBuiltinMethods(context), MarkParallelAssignments(context), diff --git a/Cython/Compiler/PyrexTypes.py b/Cython/Compiler/PyrexTypes.py index 98c57fc5f..f3618ffd2 100644 --- a/Cython/Compiler/PyrexTypes.py +++ b/Cython/Compiler/PyrexTypes.py @@ -1691,15 +1691,21 @@ class PythranExpr(CType): self.scope = scope = Symtab.CClassScope('', None, visibility="extern") scope.parent_type = self scope.directives = {} - scope.declare_cgetter( - "shape", - CPtrType(c_long_type), - pos=None, - cname="__Pyx_PythranShapeAccessor", - visibility="extern", - nogil=True) + scope.declare_var("ndim", c_long_type, pos=None, cname="value", is_cdef=True) + shape_type = c_ptr_type(c_long_type) + shape_entry = scope.declare_property( + "shape", doc="Pythran array shape", ctype=shape_type, pos=None) + shape_entry.scope.declare_cfunction( + name="shape", + type=CFuncType(shape_type, [CFuncTypeArg("self", self, pos=None)], nogil=True), + cname="__Pyx_PythranShapeAccessor", + visibility='extern', + pos=None, + ) + + return True def __eq__(self, other): diff --git a/Cython/Compiler/Symtab.py b/Cython/Compiler/Symtab.py index dcf5a9fac..17b99be11 100644 --- a/Cython/Compiler/Symtab.py +++ b/Cython/Compiler/Symtab.py @@ -109,6 +109,7 @@ class Entry(object): # doc_cname string or None C const holding the docstring # getter_cname string C func for getting property # setter_cname string C func for setting or deleting property + # is_cproperty boolean Is an inline property of an external type # is_self_arg boolean Is the "self" arg of an exttype method # is_arg boolean Is the arg of a method # is_local boolean Is a local variable @@ -183,6 +184,7 @@ class Entry(object): is_cpp_class = 0 is_const = 0 is_property = 0 + is_cproperty = 0 doc_cname = None getter_cname = None setter_cname = None @@ -2395,15 +2397,18 @@ class CClassScope(ClassScope): entry.as_variable = var_entry return entry - def declare_property(self, name, doc, pos): + def declare_property(self, name, doc, pos, ctype=None, property_scope=None): entry = self.lookup_here(name) if entry is None: - entry = self.declare(name, name, py_object_type, pos, 'private') - entry.is_property = 1 + entry = self.declare(name, name, py_object_type if ctype is None else ctype, pos, 'private') + entry.is_property = True + if ctype is not None: + entry.is_cproperty = True entry.doc = doc - entry.scope = PropertyScope(name, - outer_scope = self.global_scope(), parent_scope = self) - entry.scope.parent_type = self.parent_type + if property_scope is None: + entry.scope = PropertyScope(name, class_scope=self) + else: + entry.scope = property_scope self.property_entries.append(entry) return entry @@ -2607,6 +2612,31 @@ class PropertyScope(Scope): is_property_scope = 1 + def __init__(self, name, class_scope): + # outer scope is None for some internal properties + outer_scope = class_scope.global_scope() if class_scope.outer_scope else None + Scope.__init__(self, name, outer_scope, parent_scope=class_scope) + self.parent_type = class_scope.parent_type + self.directives = class_scope.directives + + def declare_cfunction(self, name, type, pos, *args, **kwargs): + """Declare a C property function. + """ + if type.return_type.is_void: + error(pos, "C property method cannot return 'void'") + + if type.args and type.args[0].type is py_object_type: + # Set 'self' argument type to extension type. + type.args[0].type = self.parent_scope.parent_type + elif len(type.args) != 1: + error(pos, "C property method must have a single (self) argument") + elif not (type.args[0].type.is_pyobject or type.args[0].type is self.parent_scope.parent_type): + error(pos, "C property method must have a single (object) argument") + + entry = Scope.declare_cfunction(self, name, type, pos, *args, **kwargs) + entry.is_cproperty = True + return entry + def declare_pyfunction(self, name, pos, allow_redefine=False): # Add an entry for a method. signature = get_property_accessor_signature(name) diff --git a/Cython/Includes/numpy/__init__.pxd b/Cython/Includes/numpy/__init__.pxd index 3e765498e..1bcd588f5 100644 --- a/Cython/Includes/numpy/__init__.pxd +++ b/Cython/Includes/numpy/__init__.pxd @@ -244,25 +244,33 @@ cdef extern from "numpy/arrayobject.h": cdef: # Only taking a few of the most commonly used and stable fields. - char *data - dtype descr # deprecated since NumPy 1.7 ! - PyObject* base + dtype descr # deprecated since NumPy 1.7 ! + PyObject* base # NOT PUBLIC, DO NOT USE ! @property - cdef int ndim(self): + cdef inline int ndim(self): return PyArray_NDIM(self) @property - cdef npy_intp *shape(self): + cdef inline npy_intp *shape(self): return PyArray_DIMS(self) @property - cdef npy_intp *strides(self): + cdef inline npy_intp *strides(self): return PyArray_STRIDES(self) @property - cdef npy_intp size(self): - return PyArray_SIZE(ndarray) + cdef inline npy_intp size(self) nogil: + return PyArray_SIZE(self) + + @property + cdef inline char* data(self) nogil: + """The pointer to the data buffer as a char*. + This is provided for legacy reasons to avoid direct struct field access. + For new code that needs this access, you probably want to cast the result + of `PyArray_DATA()` instead. + """ + return PyArray_BYTES(self) # Note: This syntax (function definition in pxd files) is an @@ -449,8 +457,9 @@ cdef extern from "numpy/arrayobject.h": bint PyArray_ISFORTRAN(ndarray) int PyArray_FORTRANIF(ndarray) - void* PyArray_DATA(ndarray) - char* PyArray_BYTES(ndarray) + void* PyArray_DATA(ndarray) nogil + char* PyArray_BYTES(ndarray) nogil + npy_intp* PyArray_DIMS(ndarray) npy_intp* PyArray_STRIDES(ndarray) npy_intp PyArray_DIM(ndarray, size_t) @@ -551,7 +560,7 @@ cdef extern from "numpy/arrayobject.h": bint PyArray_CheckAnyScalar(object) ndarray PyArray_GETCONTIGUOUS(ndarray) bint PyArray_SAMESHAPE(ndarray, ndarray) - npy_intp PyArray_SIZE(ndarray) + npy_intp PyArray_SIZE(ndarray) nogil npy_intp PyArray_NBYTES(ndarray) object PyArray_FROM_O(object) diff --git a/tests/run/ext_attr_getter.srctree b/tests/run/ext_attr_getter.srctree index 682b47eaf..76f059ed7 100644 --- a/tests/run/ext_attr_getter.srctree +++ b/tests/run/ext_attr_getter.srctree @@ -1,22 +1,58 @@ +# mode: run +# tag: cgetter, property + +""" PYTHON setup.py build_ext --inplace -PYTHON -c "import runner" +PYTHON run_failure_tests.py +PYTHON runner.py +""" ######## setup.py ######## from Cython.Build.Dependencies import cythonize -from Cython.Compiler.Errors import CompileError from distutils.core import setup -# force the build order +# Enforce the right build order setup(ext_modules = cythonize("foo_extension.pyx", language_level=3)) setup(ext_modules = cythonize("getter[0-9].pyx", language_level=3)) -for name in ("getter_fail0.pyx", "getter_fail1.pyx"): + +######## run_failure_tests.py ######## + +import glob +import sys + +from Cython.Build.Dependencies import cythonize +from Cython.Compiler.Errors import CompileError + +# Run the failure tests +failed_tests = [] +passed_tests = [] + +def run_test(name): + title = name + with open(name, 'r') as f: + for line in f: + if 'TEST' in line: + title = line.partition('TEST:')[2].strip() + break + sys.stderr.write("\n### TESTING: %s\n" % title) + try: cythonize(name, language_level=3) - assert False except CompileError as e: - print("\nGot expected exception, continuing\n") + sys.stderr.write("\nOK: got expected exception\n") + passed_tests.append(name) + else: + sys.stderr.write("\nFAIL: compilation did not detect the error\n") + failed_tests.append(name) + +for name in sorted(glob.glob("getter_fail*.pyx")): + run_test(name) + +assert not failed_tests, "Failed tests: %s" % failed_tests +assert passed_tests # check that tests were found at all + ######## foo.h ######## @@ -39,9 +75,9 @@ typedef struct { } FooStructOpaque; -#define PyFoo_GET0M(a) ((FooStructNominal*)a)->f0 -#define PyFoo_GET1M(a) ((FooStructNominal*)a)->f1 -#define PyFoo_GET2M(a) ((FooStructNominal*)a)->f2 +#define PyFoo_GET0M(a) (((FooStructNominal*)a)->f0) +#define PyFoo_GET1M(a) (((FooStructNominal*)a)->f1) +#define PyFoo_GET2M(a) (((FooStructNominal*)a)->f2) int PyFoo_Get0F(FooStructOpaque *f) { @@ -67,6 +103,7 @@ int *PyFoo_GetV(FooStructOpaque *f) } #endif + ######## foo_extension.pyx ######## cdef class Foo: @@ -117,6 +154,7 @@ class OpaqueFoo(Foo): def field2(self): raise AttributeError('no direct access to field2') + ######## getter0.pyx ######## # Access base Foo fields from C via aliased field names @@ -138,6 +176,7 @@ def check_pyobj(Foo f): # compare the c code to the check_pyobj in getter2.pyx return bool(f.field1) + ######## getter.pxd ######## # Access base Foo fields from C via getter functions @@ -146,26 +185,33 @@ def check_pyobj(Foo f): cdef extern from "foo.h": ctypedef class foo_extension.Foo [object FooStructOpaque, check_size ignore]: @property - cdef int fieldM0(self): + cdef inline int fieldM0(self): return PyFoo_GET0M(self) @property - cdef int fieldF1(self): + cdef inline int fieldF1(self) except -123: return PyFoo_Get1F(self) @property - cdef int fieldM2(self): + cdef inline int fieldM2(self): return PyFoo_GET2M(self) @property - cdef int *vector(self): + cdef inline int *vector(self): return PyFoo_GetV(self) + @property + cdef inline int meaning_of_life(self) except -99: + cdef int ret = 21 + ret *= 2 + return ret + int PyFoo_GET0M(Foo); # this is actually a macro ! int PyFoo_Get1F(Foo); int PyFoo_GET2M(Foo); # this is actually a macro ! int *PyFoo_GetV(Foo); + ######## getter1.pyx ######## cimport getter @@ -184,6 +230,7 @@ def vec0(getter.Foo f): def check_binop(getter.Foo f): return f.fieldF1 / 10 + ######## getter2.pyx ######## cimport getter @@ -194,27 +241,81 @@ def check_pyobj(getter.Foo f): def check_unary(getter.Foo f): return -f.fieldF1 -######## getter_fail0.pyx ######## +def check_meaning_of_life(getter.Foo f): + return f.meaning_of_life + -# Make sure not all decorators are accepted +######## getter_fail_classmethod.pyx ######## + +# TEST: Make sure not all decorators are accepted. cdef extern from "foo.h": ctypedef class foo_extension.Foo [object FooStructOpaque]: + @property @classmethod - cdef void field0(): - print('in staticmethod of Foo') + cdef inline int field0(cls): + print('in classmethod of Foo') -######## getter_fail1.pyx ######## -# Make sure not all decorators are accepted -cimport cython +######## getter_fail_dot_getter.pyx ######## + +# TEST: Make sure not all decorators are accepted. cdef extern from "foo.h": ctypedef class foo_extension.Foo [object FooStructOpaque]: - @prop.getter + @property + cdef inline int field0(self): + pass + + @field0.getter + cdef inline void field1(self): + pass + + +######## getter_fail_no_inline.pyx ######## + +# TEST: Properties must be declared "inline". + +cdef extern from "foo.h": + ctypedef class foo_extension.Foo [object FooStructOpaque]: + @property + cdef int field0(self): + pass + + +######## getter_fail_void.pyx ######## + +# TEST: Properties must have a non-void return type. + +cdef extern from "foo.h": + ctypedef class foo_extension.Foo [object FooStructOpaque]: + @property cdef void field0(self): pass + +######## getter_fail_no_args.pyx ######## + +# TEST: Properties must have the right signature. + +cdef extern from "foo.h": + ctypedef class foo_extension.Foo [object FooStructOpaque]: + @property + cdef int field0(): + pass + + +######## getter_fail_too_many_args.pyx ######## + +# TEST: Properties must have the right signature. + +cdef extern from "foo.h": + ctypedef class foo_extension.Foo [object FooStructOpaque]: + @property + cdef int field0(x, y): + pass + + ######## runner.py ######## import warnings @@ -247,6 +348,8 @@ val = getter2.check_pyobj(opaque_foo) assert val is True val = getter2.check_unary(opaque_foo) assert val == -123 +val = getter2.check_meaning_of_life(opaque_foo) +assert val == 42 try: f0 = opaque_ret.field0 diff --git a/tests/run/numpy_parallel.pyx b/tests/run/numpy_parallel.pyx index da1444c38..96a60be14 100644 --- a/tests/run/numpy_parallel.pyx +++ b/tests/run/numpy_parallel.pyx @@ -21,7 +21,7 @@ def test_parallel_numpy_arrays(): 3 4 """ - cdef Py_ssize_t i + cdef Py_ssize_t i, length cdef np.ndarray[np.int_t] x try: @@ -32,10 +32,10 @@ def test_parallel_numpy_arrays(): return x = numpy.zeros(10, dtype=numpy.int) + length = x.shape[0] - for i in prange(x.shape[0], nogil=True): + for i in prange(length, nogil=True): x[i] = i - 5 for i in x: - print i - + print(i) |