def _attributes_from_assignment(ctx: 'mypy.plugin.ClassDefContext', stmt: AssignmentStmt, auto_attribs: bool, kw_only: bool) -> Iterable[Attribute]: """Return Attribute objects that are created by this assignment. The assignments can look like this: x = attr.ib() x = y = attr.ib() x, y = attr.ib(), attr.ib() or if auto_attribs is enabled also like this: x: type x: type = default_value """ for lvalue in stmt.lvalues: lvalues, rvalues = _parse_assignments(lvalue, stmt) if len(lvalues) != len(rvalues): # This means we have some assignment that isn't 1 to 1. # It can't be an attrib. continue for lhs, rvalue in zip(lvalues, rvalues): # Check if the right hand side is a call to an attribute maker. if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr) and rvalue.callee.fullname in attr_attrib_makers): attr = _attribute_from_attrib_maker(ctx, auto_attribs, kw_only, lhs, rvalue, stmt) if attr: yield attr elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): yield _attribute_from_auto_attrib(ctx, kw_only, lhs, rvalue, stmt)
def _attributes_from_assignment(ctx: 'mypy.plugin.ClassDefContext', stmt: AssignmentStmt, auto_attribs: bool) -> Iterable[Attribute]: """Return Attribute objects that are created by this assignment. The assignments can look like this: x = attr.ib() x = y = attr.ib() x, y = attr.ib(), attr.ib() or if auto_attribs is enabled also like this: x: type x: type = default_value """ for lvalue in stmt.lvalues: lvalues, rvalues = _parse_assignments(lvalue, stmt) if len(lvalues) != len(rvalues): # This means we have some assignment that isn't 1 to 1. # It can't be an attrib. continue for lhs, rvalue in zip(lvalues, rvalues): # Check if the right hand side is a call to an attribute maker. if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr) and rvalue.callee.fullname in attr_attrib_makers): attr = _attribute_from_attrib_maker(ctx, auto_attribs, lhs, rvalue, stmt) if attr: yield attr elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): yield _attribute_from_auto_attrib(ctx, lhs, rvalue, stmt)
def generate_attr_defaults(builder: IRBuilder, cdef: ClassDef) -> None: """Generate an initialization method for default attr values (from class vars).""" cls = builder.mapper.type_to_ir[cdef.info] if cls.builtin_base: return # Pull out all assignments in classes in the mro so we can initialize them # TODO: Support nested statements default_assignments = [] for info in reversed(cdef.info.mro): if info not in builder.mapper.type_to_ir: continue for stmt in info.defn.defs.body: if (isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr) and not is_class_var(stmt.lvalues[0]) and not isinstance(stmt.rvalue, TempNode)): name = stmt.lvalues[0].name if name == '__slots__': continue if name == '__deletable__': check_deletable_declaration(builder, cls, stmt.line) continue # Skip type annotated assignments in dataclasses if is_dataclass(cdef) and stmt.type: continue default_assignments.append(stmt) if not default_assignments: return builder.enter_method(cls, '__mypyc_defaults_setup', bool_rprimitive) self_var = builder.self() for stmt in default_assignments: lvalue = stmt.lvalues[0] assert isinstance(lvalue, NameExpr) if not stmt.is_final_def and not is_constant(stmt.rvalue): builder.warning('Unsupported default attribute value', stmt.rvalue.line) # If the attribute is initialized to None and type isn't optional, # don't initialize it to anything. attr_type = cls.attr_type(lvalue.name) if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == 'builtins.None': if (not is_optional_type(attr_type) and not is_object_rprimitive(attr_type) and not is_none_rprimitive(attr_type)): continue val = builder.coerce(builder.accept(stmt.rvalue), attr_type, stmt.line) builder.add(SetAttr(self_var, lvalue.name, val, -1)) builder.add(Return(builder.true())) builder.leave_method()
def find_attr_initializers( builder: IRBuilder, cdef: ClassDef, skip: Optional[Callable[[str, AssignmentStmt], bool]] = None, ) -> Tuple[Set[str], List[AssignmentStmt]]: """Find initializers of attributes in a class body. If provided, the skip arg should be a callable which will return whether to skip generating a default for an attribute. It will be passed the name of the attribute and the corresponding AssignmentStmt. """ cls = builder.mapper.type_to_ir[cdef.info] if cls.builtin_base: return set(), [] attrs_with_defaults = set() # Pull out all assignments in classes in the mro so we can initialize them # TODO: Support nested statements default_assignments = [] for info in reversed(cdef.info.mro): if info not in builder.mapper.type_to_ir: continue for stmt in info.defn.defs.body: if (isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr) and not is_class_var(stmt.lvalues[0]) and not isinstance(stmt.rvalue, TempNode)): name = stmt.lvalues[0].name if name == '__slots__': continue if name == '__deletable__': check_deletable_declaration(builder, cls, stmt.line) continue if skip is not None and skip(name, stmt): continue attr_type = cls.attr_type(name) # If the attribute is initialized to None and type isn't optional, # doesn't initialize it to anything (special case for "# type:" comments). if isinstance( stmt.rvalue, RefExpr) and stmt.rvalue.fullname == 'builtins.None': if (not is_optional_type(attr_type) and not is_object_rprimitive(attr_type) and not is_none_rprimitive(attr_type)): continue attrs_with_defaults.add(name) default_assignments.append(stmt) return attrs_with_defaults, default_assignments
def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: # Variable declaration with no body if isinstance(stmt.rvalue, TempNode): return # Only treat marked class variables as class variables. if not (is_class_var(lvalue) or stmt.is_final_def): return typ = self.builder.load_native_type_object(self.cdef.fullname) value = self.builder.accept(stmt.rvalue) self.builder.call_c( py_setattr_op, [typ, self.builder.load_str(lvalue.name), value], stmt.line) if self.builder.non_function_scope() and stmt.is_final_def: self.builder.init_final_static(lvalue, value, self.cdef.name)
def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None: """Create IR for a class definition. This can generate both extension (native) and non-extension classes. These are generated in very different ways. In the latter case we construct a Python type object at runtime by doing the equivalent of "type(name, bases, dict)" in IR. Extension classes are defined via C structs that are generated later in mypyc.codegen.emitclass. This is the main entry point to this module. """ ir = builder.mapper.type_to_ir[cdef.info] # We do this check here because the base field of parent # classes aren't necessarily populated yet at # prepare_class_def time. if any(ir.base_mro[i].base != ir. base_mro[i + 1] for i in range(len(ir.base_mro) - 1)): builder.error("Non-trait MRO must be linear", cdef.line) if ir.allow_interpreted_subclasses: for parent in ir.mro: if not parent.allow_interpreted_subclasses: builder.error( 'Base class "{}" does not allow interpreted subclasses'.format( parent.fullname), cdef.line) # Currently, we only create non-extension classes for classes that are # decorated or inherit from Enum. Classes decorated with @trait do not # apply here, and are handled in a different way. if ir.is_ext_class: # If the class is not decorated, generate an extension class for it. type_obj = allocate_class(builder, cdef) # type: Optional[Value] non_ext = None # type: Optional[NonExtClassInfo] dataclass_non_ext = dataclass_non_ext_info(builder, cdef) else: non_ext_bases = populate_non_ext_bases(builder, cdef) non_ext_metaclass = find_non_ext_metaclass(builder, cdef, non_ext_bases) non_ext_dict = setup_non_ext_dict(builder, cdef, non_ext_metaclass, non_ext_bases) # We populate __annotations__ for non-extension classes # because dataclasses uses it to determine which attributes to compute on. # TODO: Maybe generate more precise types for annotations non_ext_anns = builder.call_c(dict_new_op, [], cdef.line) non_ext = NonExtClassInfo(non_ext_dict, non_ext_bases, non_ext_anns, non_ext_metaclass) dataclass_non_ext = None type_obj = None attrs_to_cache = [] # type: List[Tuple[Lvalue, RType]] for stmt in cdef.defs.body: if isinstance(stmt, OverloadedFuncDef) and stmt.is_property: if not ir.is_ext_class: # properties with both getters and setters in non_extension # classes not supported builder.error("Property setters not supported in non-extension classes", stmt.line) for item in stmt.items: with builder.catch_errors(stmt.line): transform_method(builder, cdef, non_ext, get_func_def(item)) elif isinstance(stmt, (FuncDef, Decorator, OverloadedFuncDef)): # Ignore plugin generated methods (since they have no # bodies to compile and will need to have the bodies # provided by some other mechanism.) if cdef.info.names[stmt.name].plugin_generated: continue with builder.catch_errors(stmt.line): transform_method(builder, cdef, non_ext, get_func_def(stmt)) elif isinstance(stmt, PassStmt): continue elif isinstance(stmt, AssignmentStmt): if len(stmt.lvalues) != 1: builder.error("Multiple assignment in class bodies not supported", stmt.line) continue lvalue = stmt.lvalues[0] if not isinstance(lvalue, NameExpr): builder.error("Only assignment to variables is supported in class bodies", stmt.line) continue # We want to collect class variables in a dictionary for both real # non-extension classes and fake dataclass ones. var_non_ext = non_ext or dataclass_non_ext if var_non_ext: add_non_ext_class_attr(builder, var_non_ext, lvalue, stmt, cdef, attrs_to_cache) if non_ext: continue # Variable declaration with no body if isinstance(stmt.rvalue, TempNode): continue # Only treat marked class variables as class variables. if not (is_class_var(lvalue) or stmt.is_final_def): continue typ = builder.load_native_type_object(cdef.fullname) value = builder.accept(stmt.rvalue) builder.call_c( py_setattr_op, [typ, builder.load_str(lvalue.name), value], stmt.line) if builder.non_function_scope() and stmt.is_final_def: builder.init_final_static(lvalue, value, cdef.name) elif isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr): # Docstring. Ignore pass else: builder.error("Unsupported statement in class body", stmt.line) if not non_ext: # That is, an extension class generate_attr_defaults(builder, cdef) create_ne_from_eq(builder, cdef) if dataclass_non_ext: assert type_obj dataclass_finalize(builder, cdef, dataclass_non_ext, type_obj) else: # Dynamically create the class via the type constructor non_ext_class = load_non_ext_class(builder, ir, non_ext, cdef.line) non_ext_class = load_decorated_class(builder, cdef, non_ext_class) # Save the decorated class builder.add(InitStatic(non_ext_class, cdef.name, builder.module_name, NAMESPACE_TYPE)) # Add the non-extension class to the dict builder.call_c(dict_set_item_op, [ builder.load_globals_dict(), builder.load_str(cdef.name), non_ext_class ], cdef.line) # Cache any cachable class attributes cache_class_attrs(builder, attrs_to_cache, cdef)
def generate_attr_defaults(self, cdef: ClassDef) -> None: """Generate an initialization method for default attr values (from class vars)""" cls = self.mapper.type_to_ir[cdef.info] if cls.builtin_base: return # Pull out all assignments in classes in the mro so we can initialize them # TODO: Support nested statements default_assignments = [] for info in reversed(cdef.info.mro): if info not in self.mapper.type_to_ir: continue for stmt in info.defn.defs.body: if (isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr) and not is_class_var(stmt.lvalues[0]) and not isinstance(stmt.rvalue, TempNode)): if stmt.lvalues[0].name == '__slots__': continue # Skip type annotated assignments in dataclasses if is_dataclass(cdef) and stmt.type: continue default_assignments.append(stmt) if not default_assignments: return self.builder.enter() self.builder.ret_types[-1] = bool_rprimitive rt_args = (RuntimeArg(SELF_NAME, RInstance(cls)),) self_var = self.builder.read(add_self_to_env(self.builder.environment, cls), -1) for stmt in default_assignments: lvalue = stmt.lvalues[0] assert isinstance(lvalue, NameExpr) if not stmt.is_final_def and not is_constant(stmt.rvalue): self.builder.warning('Unsupported default attribute value', stmt.rvalue.line) # If the attribute is initialized to None and type isn't optional, # don't initialize it to anything. attr_type = cls.attr_type(lvalue.name) if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == 'builtins.None': if (not is_optional_type(attr_type) and not is_object_rprimitive(attr_type) and not is_none_rprimitive(attr_type)): continue val = self.builder.coerce(self.accept(stmt.rvalue), attr_type, stmt.line) self.add(SetAttr(self_var, lvalue.name, val, -1)) self.add(Return(self.primitive_op(true_op, [], -1))) blocks, env, ret_type, _ = self.builder.leave() ir = FuncIR( FuncDecl('__mypyc_defaults_setup', cls.name, self.module_name, FuncSignature(rt_args, ret_type)), blocks, env) self.builder.functions.append(ir) cls.methods[ir.name] = ir
def attr_class_maker_callback(attr_classes: Dict[TypeInfo, List[Attribute]], ctx: ClassDefContext, auto_attribs_default: bool = False) -> None: """Add necessary dunder methods to classes decorated with attr.s. attrs is a package that lets you define classes without writing dull boilerplate code. At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or annotated variables if auto_attribs=True), then depending on how the decorator is called, it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs into properties. See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. """ info = ctx.cls.info # auto_attribs means we also generate Attributes from annotated variables. auto_attribs = _attrs_get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) if ctx.api.options.python_version[0] < 3: if auto_attribs: ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) return if not info.defn.base_type_exprs: # Note: This does not catch subclassing old-style classes. ctx.api.fail("attrs only works with new-style classes", info.defn) return # First, walk the body looking for attribute definitions. # They will look like this: # x = attr.ib() # x = y = attr.ib() # x, y = attr.ib(), attr.ib() # or if auto_attribs is enabled also like this: # x: type # x: type = default_value own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): for lvalue in stmt.lvalues: # To handle all types of assignments we just convert everything # to a matching lists of lefts and rights. lhss = [] # type: List[NameExpr] rvalues = [] # type: List[Expression] if isinstance(lvalue, (TupleExpr, ListExpr)): if all( isinstance(item, NameExpr) for item in lvalue.items): lhss = cast(List[NameExpr], lvalue.items) if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): rvalues = stmt.rvalue.items elif isinstance(lvalue, NameExpr): lhss = [lvalue] rvalues = [stmt.rvalue] if len(lhss) != len(rvalues): # This means we have some assignment that isn't 1 to 1. # It can't be an attrib. continue for lhs, rvalue in zip(lhss, rvalues): typ = stmt.type name = lhs.name # Check if the right hand side is a call to an attribute maker. if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr) and rvalue.callee.fullname in attr_attrib_makers): if auto_attribs and not stmt.new_syntax: # auto_attribs requires annotation on every attr.ib. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, stmt) continue if len(stmt.lvalues) > 1: ctx.api.fail("Too many names for one attribute", stmt) continue # Look for default=<something> in the call. # TODO: Check for attr.NOTHING attr_has_default = bool( _attrs_get_argument(rvalue, 'default')) # If the type isn't set through annotation but it is passed through type= # use that. type_arg = _attrs_get_argument(rvalue, 'type') if type_arg and not typ: try: un_type = expr_to_unanalyzed_type(type_arg) except TypeTranslationError: ctx.api.fail('Invalid argument to type', type_arg) else: typ = ctx.api.anal_type(un_type) if typ and isinstance( lhs.node, Var) and not lhs.node.type: # If there is no annotation, add one. lhs.node.type = typ lhs.is_inferred_def = False # If the attrib has a converter function take the type of the first # argument as the init type. # Note: convert is deprecated but works the same as converter. converter = _attrs_get_argument(rvalue, 'converter') convert = _attrs_get_argument(rvalue, 'convert') if convert and converter: ctx.api.fail( "Can't pass both `convert` and `converter`.", rvalue) elif convert: converter = convert if (converter and isinstance(converter, RefExpr) and converter.node and isinstance(converter.node, FuncBase) and converter.node.type and isinstance( converter.node.type, CallableType) and converter.node.type.arg_types): typ = converter.node.type.arg_types[0] # Does this even have to go in init. init = _attrs_get_bool_argument( ctx, rvalue, 'init', True) # When attrs are defined twice in the same body we want to use # the 2nd definition in the 2nd location. So remove it from the # OrderedDict. auto_attribs doesn't work that way. if not auto_attribs and name in own_attrs: del own_attrs[name] own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) elif auto_attribs and typ and stmt.new_syntax and not is_class_var( lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(rvalue, TempNode) own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) elif isinstance(stmt, Decorator): # Look for attr specific decorators. ('x.default' and 'x.validator') remove_me = [] for func_decorator in stmt.decorators: if (isinstance(func_decorator, MemberExpr) and isinstance(func_decorator.expr, NameExpr) and func_decorator.expr.name in own_attrs): if func_decorator.name == 'default': # This decorator lets you set a default after the fact. own_attrs[func_decorator.expr.name].has_default = True if func_decorator.name in ('default', 'validator'): # These are decorators on the attrib object that only exist during # class creation time. In order to not trigger a type error later we # just remove them. This might leave us with a Decorator with no # decorators (Emperor's new clothes?) # TODO: It would be nice to type-check these rather than remove them. # default should be Callable[[], T] # validator should be Callable[[Any, 'Attribute', T], Any] # where T is the type of the attribute. remove_me.append(func_decorator) for dec in remove_me: stmt.decorators.remove(dec) taken_attr_names = set(own_attrs) super_attrs = [] # Traverse the MRO and collect attributes from the parents. for super_info in info.mro[1:-1]: if super_info in attr_classes: for a in attr_classes[super_info]: # Only add an attribute if it hasn't been defined before. This # allows for overwriting attribute definitions by subclassing. if a.name not in taken_attr_names: super_attrs.append(a) taken_attr_names.add(a.name) attributes = super_attrs + list(own_attrs.values()) # Save the attributes so that subclasses can reuse them. # TODO: This doesn't work with incremental mode if the parent class is in a different file. attr_classes[info] = attributes if ctx.api.options.disallow_untyped_defs: for attribute in attributes: if attribute.type is None: # This is a compromise. If you don't have a type here then the __init__ will # be untyped. But since the __init__ is added it's pointing at the decorator. # So instead we just show the error in the assignment, which is where you # would fix the issue. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, attribute.context) # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False for attribute in attributes: if not attribute.has_default and last_default: ctx.api.fail( "Non-default attributes not allowed after default attributes.", attribute.context) last_default = attribute.has_default adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) if _attrs_get_decorator_bool_argument(ctx, 'init', True): # Generate the __init__ method. adder.add_method('__init__', [ attribute.argument() for attribute in attributes if attribute.init ], NoneTyp()) for stmt in ctx.cls.defs.body: # The type of classmethods will be wrong because it's based on the parent's __init__. # Set it correctly. if isinstance(stmt, Decorator) and stmt.func.is_class: func_type = stmt.func.type if isinstance(func_type, CallableType): func_type.arg_types[0] = ctx.api.class_type(info) if _attrs_get_decorator_bool_argument(ctx, 'frozen', False): # If the class is frozen then all the attributes need to be turned into properties. for attribute in attributes: node = info.names[attribute.name].node assert isinstance(node, Var) node.is_initialized_in_class = False node.is_property = True if _attrs_get_decorator_bool_argument(ctx, 'cmp', True): # For __ne__ and __eq__ the type is: # def __ne__(self, other: object) -> bool bool_type = ctx.api.named_type('__builtins__.bool') object_type = ctx.api.named_type('__builtins__.object') args = [ Argument(Var('other', object_type), object_type, None, ARG_POS) ] for method in ['__ne__', '__eq__']: adder.add_method(method, args, bool_type) # For the rest we use: # AT = TypeVar('AT') # def __lt__(self: AT, other: AT) -> bool # This way comparisons with subclasses will work correctly. tvd = TypeVarDef('AT', 'AT', 1, [], object_type) tvd_type = TypeVarType(tvd) args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] for method in ['__lt__', '__le__', '__gt__', '__ge__']: adder.add_method(method, args, bool_type, self_type=tvd_type, tvd=tvd)