def construct_dec_post_load(schema): name = class_name(schema.name) name_lower = name.lower() fn_args = ast.arguments( args=[ ast.arg(arg="self", annotation=None), ast.arg(arg=name_lower, annotation=None) ], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[], ) fn_body = [ ast.Return(value=ast.Call(func=ast.Name(id=name), args=[ast.Name(id=name_lower)], keywords=[])) ] # By convention, the helper loading function is called make_class return ast.FunctionDef( name="make_" + name_lower, args=fn_args, body=fn_body, decorator_list=[ast.Name(id="post_load")], returns=None, )
def get_klass_constructor(self, properties: PropertiesType, requireds: RequiredType) -> ast.FunctionDef: # Prepare body fn_body = [] # Default value for `data` argument if len(properties) > 0: fn_body.append( ast.Assign( targets=[ast.Name(id="data")], value=ast.BoolOp(op=ast.Or(), values=[ ast.Name(id="data"), ast.Dict(keys=[], values=[]) ]), )) for key in sorted(properties.keys()): # Get default value property_ = properties[key] is_required = key in requireds value = self.get_member_value(key, property_, is_required=is_required) # Build assign expression attribute = ast.Assign( targets=[ast.Attribute(value=ast.Name(id="self"), attr=key)], value=value) # Add to body fn_body.append(attribute) # Bundle function arguments and keywords fn_arguments = ast.arguments( args=[ ast.arg(arg="self", annotation=None), ast.arg(arg="data", annotation=None) ], vararg=None, kwarg=None, kwonlyargs=[], kw_defaults=[], defaults=[ast.NameConstant(value=None)], ) # Generate class constructor fn_init = ast.FunctionDef(name="__init__", args=fn_arguments, body=fn_body, decorator_list=[], returns=None) # Return constructor return fn_init
def map_property_as_array(self, property_: PropertyType, value: ast.AST) -> ast.AST: """ If array and `items` has `$ref` wrap it into a list comprehension and map array's elements """ # Exit early if doesn't needs to wrap items = property_.get("items") if isinstance(items, list): # We don't support tuple definition yet raise NotImplementedError( "Tuple items for type 'array' not supported: {}".format( property_)) elif isinstance(items, dict): if "oneOf" in items: ref = items["oneOf"][0].get("$ref") if ref is None: return value # Don't wrap if type is a primitive ref = self.definitions[ref] if self.definition_is_primitive_alias(ref): return value # Wrap value ref_title = ref["title"] return ast.ListComp( elt=ast.Call( func=ast.Name(id=ref_title), args=[ast.Name(id="v")], keywords=[], starargs=None, kwargs=None, ), generators=[ ast.comprehension(target=ast.Name(id="v"), iter=value, ifs=[], is_async=0) ], ) return value
def get_key_from_data_object(self, key: str) -> ast.Call: return ast.Call( func=ast.Attribute(value=ast.Name(id="data"), attr="get"), args=[ast.Str(s=key)], keywords=[], starargs=None, kwargs=None, )
def get_nested_type(self, prop): """ Scrap the definition from the reference string """ nested_schema_name = search("#/definitions/(.*)$", prop["$ref"]) attr_type = nested_schema_name.group(1) attr_args = [ast.Name(id=upper_first_letter(attr_type) + "Schema")] return "Dict", attr_args
def _make_field(self, field: str, args: List, keywords: List) -> ast.Call: return ast.Call( func=ast.Attribute(value=ast.Name(id="fields_"), attr=field), args=args, keywords=keywords, starargs=None, kwargs=None, )
def map_property_as_array(self, property_: PropertyType, value: ast.AST) -> ast.ListComp: """ If array has `items` as 'object' type with `$ref` wrap it into a list comprehension and map array's elements """ # Exit early if doesn't needs to wrap items = property_.get("items") if isinstance(items, dict): one_of = items.get("oneOf") ref = one_of[0]["$ref"] if one_of else None elif isinstance(items, list): raise NotImplementedError( "Tuple for 'array' non supported: {}".format(property_)) else: ref = None if property_.get("type") != "array" or ref is None: return value # Don't wrap if type is a primitive ref = self.definitions[ref] if self.definition_is_primitive_alias(ref): return value # Wrap value ref_title = ref["title"] return ast.ListComp( elt=ast.Call( func=ast.Name(id=ref_title), args=[ast.Name(id="v")], keywords=[], starargs=None, kwargs=None, ), generators=[ ast.comprehension(target=ast.Name(id="v"), iter=value, ifs=[], is_async=0) ], )
def get_partial_annotation_from_definition( self, property_: PropertyType) -> ast.AST: # Map property type to annotation property_type = property_["type"] # ...map object if property_type == "object": if "oneOf" in property_: ref = self.definitions[property_["oneOf"][0]["$ref"]] if self.definition_is_primitive_alias(ref): annotation = self.get_partial_annotation_from_definition( ref) else: annotation = ast.Name(id=ref["title"]) elif "additionalProperties" in property_: object_name = self.definitions[ property_["additionalProperties"]["$ref"]]["title"] annotation = ast.Subscript( value=ast.Name(id="Dict"), slice=ast.Index(value=ast.Tuple( elts=[ast.Name(id="str"), ast.Name(id=object_name)])), ) else: annotation = ast.Name(id="Dict") # ... map array elif property_type == "array": items = property_.get("items") if isinstance(items, dict): item_annotation = self.get_partial_annotation_from_definition( items) elif isinstance(items, list): raise NotImplementedError( "Tuple for 'array' is not supported: {}".format(property_)) else: item_annotation = ast.Name(id="Any") annotation = ast.Subscript(value=ast.Name(id="List"), slice=ast.Index(value=item_annotation)) # ... map scalar else: python_type = PYTHON_ANNOTATION_MAP[property_["type"]] annotation = ast.Name(id=python_type) # Return return annotation
def get_annotation_from_definition(self, property_: PropertyType, is_required: bool = False) -> ast.AST: annotation = self.get_partial_annotation_from_definition(property_) # Make annotation optional if no default value if not is_required and "default" not in property_: annotation = ast.Subscript(value=ast.Name(id="Optional"), slice=ast.Index(value=annotation)) # Return return annotation
def _get_dict_comprehension_for_property(self, key, property_): # key, value comp_key = ast.Name(id="k") comp_value = ast.Call( func=ast.Name(id="Value"), args=[ast.Name(id="v")], keywords=[], starargs=None, kwargs=None, ) generator = ast.comprehension( target=ast.Tuple(elts=[ast.Name( id="k"), ast.Name(id="v")]), iter=ast.Call( func=ast.Attribute( value=self.get_key_with_default_for_data_object( key, property_), attr="iteritems", ), args=[], keywords=[], starargs=None, kwargs=None, ), ifs=[], is_async=0, ) # Dit comprehension dict_comp = ast.DictComp(key=comp_key, value=comp_value, generators=[generator]) # Return node return dict_comp
def get_klass_constructor(self, properties, required): # Prepare body body = [] for key in sorted(properties.keys()): # Get default value property_ = properties[key] value = self._get_member_value(key, property_, required) # Build assign expression attribute = ast.Assign(targets=[ast.Name(id=key)], value=value) # Add to body body.append(attribute) # Return constructor return body
def _map_property_if_array(self, property_: PropertyType, value: ast.AST) -> ast.AST: """ If array and `items` has `$ref` wrap it into a list comprehension and map array's elements """ # Exit early if doesn't needs to wrap property_items = property_.get("items", {}) if isinstance(property_items, dict) and "oneOf" in property_items: refs = (i.get("$ref") for i in property_items["oneOf"]) refs = [r for r in refs if r is not None] elif isinstance(property_items, list): refs = [ i["$ref"] for i in property_.get("items", []) if "$ref" in i ] else: refs = [] if property_.get("type") != "array": # or len(refs) == 0: return value if len(refs) == 0: return self._set_item_type_scalar(property_, value) # Only support one custom type for now if len(refs) > 1: raise NotImplementedError( "{}: we only support one $ref per array".format(self)) # Don't wrap if type is a primitive ref = self.definitions[refs[0]] if self.definition_is_primitive_alias(ref): return self._set_item_type_scalar(property_, value) # Where the array type references a definition, make a nested field with # the type of the item schema ref_title = upper_first_letter(ref["title"]) for node in ast.walk(value): if isinstance(node, ast.Call): x = self._make_field("Nested", [ast.Name(id=ref_title + "Schema")], []) node.args = [x] + node.args return value
def klass(self, definition): # Build class properties class_body = [] properties = definition.get("properties") if properties: required = definition.get("required", ()) class_body.extend(self.get_klass_constructor(properties, required)) else: class_body.append(ast.Pass()) # Create class definition class_def = ast.ClassDef( name=upper_first_letter(definition["title"]) + "Schema", bases=[ast.Name(id="Schema")], body=class_body, decorator_list=[], keywords=[], ) # Add to module's body return class_def
def coerce_to_nested_object(self, property_: PropertyType, value: ast.AST) -> ast.AST: if "oneOf" in property_: ref = property_["oneOf"][0].get("$ref") if ref is not None: # Don't wrap if type is a primitive ref = self.definitions[ref] if not self.definition_is_primitive_alias(ref): ref_title = ref["title"] value = ast.IfExp( test=ast.Compare( left=value, ops=[ast.Is()], comparators=[ast.NameConstant(value=None)]), body=ast.NameConstant(value=None), orelse=ast.Call(func=ast.Name(id=ref_title), args=[value], keywords=[]), ) return value
def slice_key_from_data_object(self, key: str) -> ast.Subscript: return ast.Subscript(value=ast.Name(id="data"), slice=ast.Index(value=ast.Str(s=key)))