def infer_dict(node, context=None):
    """Try to infer a dict call to a Dict node.

    The function treats the following cases:

        * dict()
        * dict(mapping)
        * dict(iterable)
        * dict(iterable, **kwargs)
        * dict(mapping, **kwargs)
        * dict(**kwargs)

    If a case can't be infered, we'll fallback to default inference.
    """
    has_keywords = lambda args: all(
        isinstance(arg, nodes.Keyword) for arg in args)
    if not node.args and not node.kwargs:
        # dict()
        return nodes.Dict()
    elif has_keywords(node.args) and node.args:
        # dict(a=1, b=2, c=4)
        items = [(nodes.Const(arg.arg), arg.value) for arg in node.args]
    elif (len(node.args) >= 2 and has_keywords(node.args[1:])):
        # dict(some_iterable, b=2, c=4)
        elts = _get_elts(node.args[0], context)
        keys = [(nodes.Const(arg.arg), arg.value) for arg in node.args[1:]]
        items = elts + keys
    elif len(node.args) == 1:
        items = _get_elts(node.args[0], context)
    else:
        raise UseInferenceDefault()

    empty = nodes.Dict()
    empty.items = items
    return empty
Exemplo n.º 2
0
 def visit_dict(self, node, parent, assign_ctx=None):
     """visit a Dict node by returning a fresh instance of it"""
     newnode = new.Dict()
     _lineno_parent(node, newnode, parent)
     newnode.items = list(
         self._visit_dict_items(node, parent, newnode, assign_ctx))
     return newnode
Exemplo n.º 3
0
 def visit_dict(self, node, parent):
     """visit a Dict node by returning a fresh instance of it"""
     newnode = new.Dict()
     _lineno_parent(node, newnode, parent)
     newnode.items = [(self.visit(key, newnode), self.visit(value, newnode))
                      for key, value in zip(node.keys, node.values)]
     return newnode
Exemplo n.º 4
0
def infer_dict(node, context=None):
    """Try to infer a dict call to a Dict node.

    The function treats the following cases:

        * dict()
        * dict(mapping)
        * dict(iterable)
        * dict(iterable, **kwargs)
        * dict(mapping, **kwargs)
        * dict(**kwargs)

    If a case can't be inferred, we'll fallback to default inference.
    """
    call = arguments.CallSite.from_call(node, context=context)
    if call.has_invalid_arguments() or call.has_invalid_keywords():
        raise UseInferenceDefault

    args = call.positional_arguments
    kwargs = list(call.keyword_arguments.items())

    if not args and not kwargs:
        # dict()
        return nodes.Dict()
    elif kwargs and not args:
        # dict(a=1, b=2, c=4)
        items = [(nodes.Const(key), value) for key, value in kwargs]
    elif len(args) == 1 and kwargs:
        # dict(some_iterable, b=2, c=4)
        elts = _get_elts(args[0], context)
        keys = [(nodes.Const(key), value) for key, value in kwargs]
        items = elts + keys
    elif len(args) == 1:
        items = _get_elts(args[0], context)
    else:
        raise UseInferenceDefault()

    value = nodes.Dict(col_offset=node.col_offset,
                       lineno=node.lineno,
                       parent=node.parent)
    value.postinit(items)
    return value
Exemplo n.º 5
0
def transform(cls):
    if cls.name in CLASS_NAME_BLACKLIST:
        return

    if cls.name == 'StormFoundationDB':
        # _fields get added automagically by mongoengine
        if '_fields' not in cls.locals:
            cls.locals['_fields'] = [nodes.Dict()]

    if cls.name.endswith('DB'):
        # mongoengine explicitly declared "id" field on each class so we teach pylint about that
        property_name = 'id'
        node = scoped_nodes.Class(property_name, None)
        cls.locals[property_name] = [node]
Exemplo n.º 6
0
def transform(cls):
    if cls.name in CLASS_NAME_BLACKLIST:
        return

    if cls.name.endswith('API') or 'schema' in cls.locals:
        # This is a class which defines attributes in "schema" variable using json schema.
        # Those attributes are then assigned during run time inside the constructor
        fqdn = cls.qname()
        module_name, class_name = fqdn.rsplit('.', 1)

        module = __import__(module_name, fromlist=[class_name])
        actual_cls = getattr(module, class_name)

        schema = actual_cls.schema

        if not isinstance(schema, dict):
            # Not a class we are interested in
            return

        properties = schema.get('properties', {})
        for property_name, property_data in six.iteritems(properties):
            property_name = property_name.replace(
                '-', '_')  # Note: We do the same in Python code
            property_type = property_data.get('type', None)

            if isinstance(property_type, (list, tuple)):
                # Hack for attributes with multiple types (e.g. string, null)
                property_type = property_type[0]

            if property_type == 'object':
                node = nodes.Dict()
            elif property_type == 'array':
                node = nodes.List()
            elif property_type == 'integer':
                node = scoped_nodes.builtin_lookup('int')[1][0]
            elif property_type == 'number':
                node = scoped_nodes.builtin_lookup('float')[1][0]
            elif property_type == 'string':
                node = scoped_nodes.builtin_lookup('str')[1][0]
            elif property_type == 'boolean':
                node = scoped_nodes.builtin_lookup('bool')[1][0]
            elif property_type == 'null':
                node = scoped_nodes.builtin_lookup('None')[1][0]
            else:
                # Unknown type
                node = scoped_nodes.Class(property_name, None)

            cls.locals[property_name] = [node]
Exemplo n.º 7
0
def transform(cls):
    if cls.name in CLASS_NAME_BLACKLIST:
        return

    if cls.name.endswith("API") or "schema" in cls.locals:
        # This is a class which defines attributes in "schema" variable using json schema.
        # Those attributes are then assigned during run time inside the constructor
        fqdn = cls.qname()
        module_name, class_name = fqdn.rsplit(".", 1)

        module = __import__(module_name, fromlist=[class_name])
        actual_cls = getattr(module, class_name)

        schema = actual_cls.schema

        if not isinstance(schema, dict):
            # Not a class we are interested in
            return

        properties = schema.get("properties", {})
        for property_name, property_data in six.iteritems(properties):
            property_name = property_name.replace(
                "-", "_")  # Note: We do the same in Python code
            property_type = property_data.get("type", None)

            if isinstance(property_type, (list, tuple)):
                # Hack for attributes with multiple types (e.g. string, null)
                property_type = property_type[0]

            if property_type == "object":
                node = nodes.Dict()
            elif property_type == "array":
                node = nodes.List()
            elif property_type == "integer":
                node = scoped_nodes.builtin_lookup("int")[1][0]
            elif property_type == "number":
                node = scoped_nodes.builtin_lookup("float")[1][0]
            elif property_type == "string":
                node = scoped_nodes.builtin_lookup("str")[1][0]
            elif property_type == "boolean":
                node = scoped_nodes.builtin_lookup("bool")[1][0]
            elif property_type == "null":
                node = scoped_nodes.builtin_lookup("None")[1][0]
            else:
                # Unknown type
                node = astroid.ClassDef(property_name, None)

            cls.locals[property_name] = [node]
Exemplo n.º 8
0
def _getattr(self, name, *args, **kw):
    try:
        return Module_getattr(self, name, *args, **kw)
    except NotFoundError, e:
        if self.name.startswith('erp5.'):
            raise

        real_module = __import__(self.name, fromlist=[self.name], level=0)
        try:
            attr = getattr(real_module, name)
        except AttributeError:
            raise e

        # REQUEST object (or any object non acquisition-wrapped)
        if (isinstance(attr, str)
                and attr == '<Special Object Used to Force Acquisition>'):
            raise e

        try:
            origin_module_name = attr.__module__
        except AttributeError:
            from astroid import nodes
            if isinstance(attr, dict):
                ast = nodes.Dict(attr)
            elif isinstance(attr, list):
                ast = nodes.List(attr)
            elif isinstance(attr, tuple):
                ast = nodes.Tuple(attr)
            elif isinstance(attr, set):
                ast = nodes.Set(attr)
            else:
                try:
                    ast = nodes.Const(attr)
                except Exception:
                    raise e
        else:
            if self.name == origin_module_name:
                raise

            # ast_from_class() actually works for any attribute of a Module
            try:
                ast = MANAGER.ast_from_class(attr)
            except AstroidBuildingException:
                raise e

        self.locals[name] = [ast]
        return [ast]
Exemplo n.º 9
0
    def infer_argument(self, funcnode, name, context):
        """infer a function argument value according to the call context

        Arguments:
            funcnode: The function being called.
            name: The name of the argument whose value is being inferred.
            context: Inference context object
        """
        if name in self.duplicated_keywords:
            raise exceptions.InferenceError(
                "The arguments passed to {func!r} "
                " have duplicate keywords.",
                call_site=self,
                func=funcnode,
                arg=name,
                context=context,
            )

        # Look into the keywords first, maybe it's already there.
        try:
            return self.keyword_arguments[name].infer(context)
        except KeyError:
            pass

        # Too many arguments given and no variable arguments.
        if len(self.positional_arguments) > len(funcnode.args.args):
            if not funcnode.args.vararg and not funcnode.args.posonlyargs:
                raise exceptions.InferenceError(
                    "Too many positional arguments "
                    "passed to {func!r} that does "
                    "not have *args.",
                    call_site=self,
                    func=funcnode,
                    arg=name,
                    context=context,
                )

        positional = self.positional_arguments[:len(funcnode.args.args)]
        vararg = self.positional_arguments[len(funcnode.args.args):]
        argindex = funcnode.args.find_argname(name)[0]
        kwonlyargs = {arg.name for arg in funcnode.args.kwonlyargs}
        kwargs = {
            key: value
            for key, value in self.keyword_arguments.items()
            if key not in kwonlyargs
        }
        # If there are too few positionals compared to
        # what the function expects to receive, check to see
        # if the missing positional arguments were passed
        # as keyword arguments and if so, place them into the
        # positional args list.
        if len(positional) < len(funcnode.args.args):
            for func_arg in funcnode.args.args:
                if func_arg.name in kwargs:
                    arg = kwargs.pop(func_arg.name)
                    positional.append(arg)

        if argindex is not None:
            # 2. first argument of instance/class method
            if argindex == 0 and funcnode.type in ("method", "classmethod"):
                if context.boundnode is not None:
                    boundnode = context.boundnode
                else:
                    # XXX can do better ?
                    boundnode = funcnode.parent.frame()

                if isinstance(boundnode, nodes.ClassDef):
                    # Verify that we're accessing a method
                    # of the metaclass through a class, as in
                    # `cls.metaclass_method`. In this case, the
                    # first argument is always the class.
                    method_scope = funcnode.parent.scope()
                    if method_scope is boundnode.metaclass():
                        return iter((boundnode, ))

                if funcnode.type == "method":
                    if not isinstance(boundnode, bases.Instance):
                        boundnode = boundnode.instantiate_class()
                    return iter((boundnode, ))
                if funcnode.type == "classmethod":
                    return iter((boundnode, ))
            # if we have a method, extract one position
            # from the index, so we'll take in account
            # the extra parameter represented by `self` or `cls`
            if funcnode.type in ("method", "classmethod"):
                argindex -= 1
            # 2. search arg index
            try:
                return self.positional_arguments[argindex].infer(context)
            except IndexError:
                pass

        if funcnode.args.kwarg == name:
            # It wants all the keywords that were passed into
            # the call site.
            if self.has_invalid_keywords():
                raise exceptions.InferenceError(
                    "Inference failed to find values for all keyword arguments "
                    "to {func!r}: {unpacked_kwargs!r} doesn't correspond to "
                    "{keyword_arguments!r}.",
                    keyword_arguments=self.keyword_arguments,
                    unpacked_kwargs=self._unpacked_kwargs,
                    call_site=self,
                    func=funcnode,
                    arg=name,
                    context=context,
                )
            kwarg = nodes.Dict(
                lineno=funcnode.args.lineno,
                col_offset=funcnode.args.col_offset,
                parent=funcnode.args,
            )
            kwarg.postinit([(nodes.const_factory(key), value)
                            for key, value in kwargs.items()])
            return iter((kwarg, ))
        if funcnode.args.vararg == name:
            # It wants all the args that were passed into
            # the call site.
            if self.has_invalid_arguments():
                raise exceptions.InferenceError(
                    "Inference failed to find values for all positional "
                    "arguments to {func!r}: {unpacked_args!r} doesn't "
                    "correspond to {positional_arguments!r}.",
                    positional_arguments=self.positional_arguments,
                    unpacked_args=self._unpacked_args,
                    call_site=self,
                    func=funcnode,
                    arg=name,
                    context=context,
                )
            args = nodes.Tuple(
                lineno=funcnode.args.lineno,
                col_offset=funcnode.args.col_offset,
                parent=funcnode.args,
            )
            args.postinit(vararg)
            return iter((args, ))

        # Check if it's a default parameter.
        try:
            return funcnode.args.default_value(name).infer(context)
        except exceptions.NoDefault:
            pass
        raise exceptions.InferenceError(
            "No value found for argument {arg} to {func!r}",
            call_site=self,
            func=funcnode,
            arg=name,
            context=context,
        )
Exemplo n.º 10
0
 def visit_dict(self, node, parent):
     """visit a Dict node by returning a fresh instance of it"""
     newnode = nodes.Dict(node.lineno, node.col_offset, parent)
     items = list(self._visit_dict_items(node, parent, newnode))
     newnode.postinit(items)
     return newnode
Exemplo n.º 11
0
 def _build_dict_with_elements(elements):
     new_node = nodes.Dict(col_offset=node.col_offset,
                           lineno=node.lineno,
                           parent=node.parent)
     new_node.postinit(elements)
     return new_node
Exemplo n.º 12
0
    def infer_argument(self, funcnode, name, context):
        """infer a function argument value according to the call context"""
        if name in self.duplicated_keywords:
            raise exceptions.InferenceError(name)

        # Look into the keywords first, maybe it's already there.
        try:
            return self.keyword_arguments[name].infer(context)
        except KeyError:
            pass

        # Too many arguments given and no variable arguments.
        if len(self.positional_arguments) > len(funcnode.args.args):
            if not funcnode.args.vararg:
                raise exceptions.InferenceError(name)

        positional = self.positional_arguments[:len(funcnode.args.args)]
        vararg = self.positional_arguments[len(funcnode.args.args):]
        argindex = funcnode.args.find_argname(name)[0]
        kwonlyargs = set(arg.name for arg in funcnode.args.kwonlyargs)
        kwargs = {
            key: value
            for key, value in self.keyword_arguments.items()
            if key not in kwonlyargs
        }
        # If there are too few positionals compared to
        # what the function expects to receive, check to see
        # if the missing positional arguments were passed
        # as keyword arguments and if so, place them into the
        # positional args list.
        if len(positional) < len(funcnode.args.args):
            for func_arg in funcnode.args.args:
                if func_arg.name in kwargs:
                    arg = kwargs.pop(func_arg.name)
                    positional.append(arg)

        if argindex is not None:
            # 2. first argument of instance/class method
            if argindex == 0 and funcnode.type in ('method', 'classmethod'):
                if context.boundnode is not None:
                    boundnode = context.boundnode
                else:
                    # XXX can do better ?
                    boundnode = funcnode.parent.frame()
                if funcnode.type == 'method':
                    if not isinstance(boundnode, bases.Instance):
                        boundnode = bases.Instance(boundnode)
                    return iter((boundnode, ))
                if funcnode.type == 'classmethod':
                    return iter((boundnode, ))
            # if we have a method, extract one position
            # from the index, so we'll take in account
            # the extra parameter represented by `self` or `cls`
            if funcnode.type in ('method', 'classmethod'):
                argindex -= 1
            # 2. search arg index
            try:
                return self.positional_arguments[argindex].infer(context)
            except IndexError:
                pass

        if funcnode.args.kwarg == name:
            # It wants all the keywords that were passed into
            # the call site.
            if self.has_invalid_keywords():
                raise exceptions.InferenceError
            kwarg = nodes.Dict()
            kwarg.lineno = funcnode.args.lineno
            kwarg.col_offset = funcnode.args.col_offset
            kwarg.parent = funcnode.args
            items = [(nodes.const_factory(key), value)
                     for key, value in kwargs.items()]
            kwarg.items = items
            return iter((kwarg, ))
        elif funcnode.args.vararg == name:
            # It wants all the args that were passed into
            # the call site.
            if self.has_invalid_arguments():
                raise exceptions.InferenceError
            args = nodes.Tuple()
            args.lineno = funcnode.args.lineno
            args.col_offset = funcnode.args.col_offset
            args.parent = funcnode.args
            args.elts = vararg
            return iter((args, ))

        # Check if it's a default parameter.
        try:
            return funcnode.args.default_value(name).infer(context)
        except exceptions.NoDefault:
            pass
        raise exceptions.InferenceError(name)
Exemplo n.º 13
0
def infer_enum_class(node):
    """Specific inference for enums."""
    for basename in (b for cls in node.mro() for b in cls.basenames):
        if basename not in ENUM_BASE_NAMES:
            continue
        if node.root().name == "enum":
            # Skip if the class is directly from enum module.
            break
        dunder_members = {}
        target_names = set()
        for local, values in node.locals.items():
            if any(not isinstance(value, nodes.AssignName) for value in values):
                continue

            stmt = values[0].statement(future=True)
            if isinstance(stmt, nodes.Assign):
                if isinstance(stmt.targets[0], nodes.Tuple):
                    targets = stmt.targets[0].itered()
                else:
                    targets = stmt.targets
            elif isinstance(stmt, nodes.AnnAssign):
                targets = [stmt.target]
            else:
                continue

            inferred_return_value = None
            if isinstance(stmt, nodes.Assign):
                if isinstance(stmt.value, nodes.Const):
                    if isinstance(stmt.value.value, str):
                        inferred_return_value = repr(stmt.value.value)
                    else:
                        inferred_return_value = stmt.value.value
                else:
                    inferred_return_value = stmt.value.as_string()

            new_targets = []
            for target in targets:
                if isinstance(target, nodes.Starred):
                    continue
                target_names.add(target.name)
                # Replace all the assignments with our mocked class.
                classdef = dedent(
                    """
                class {name}({types}):
                    @property
                    def value(self):
                        return {return_value}
                    @property
                    def name(self):
                        return "{name}"
                """.format(
                        name=target.name,
                        types=", ".join(node.basenames),
                        return_value=inferred_return_value,
                    )
                )
                if "IntFlag" in basename:
                    # Alright, we need to add some additional methods.
                    # Unfortunately we still can't infer the resulting objects as
                    # Enum members, but once we'll be able to do that, the following
                    # should result in some nice symbolic execution
                    classdef += INT_FLAG_ADDITION_METHODS.format(name=target.name)

                fake = AstroidBuilder(
                    AstroidManager(), apply_transforms=False
                ).string_build(classdef)[target.name]
                fake.parent = target.parent
                for method in node.mymethods():
                    fake.locals[method.name] = [method]
                new_targets.append(fake.instantiate_class())
                dunder_members[local] = fake
            node.locals[local] = new_targets
        members = nodes.Dict(parent=node)
        members.postinit(
            [
                (nodes.Const(k, parent=members), nodes.Name(v.name, parent=members))
                for k, v in dunder_members.items()
            ]
        )
        node.locals["__members__"] = [members]
        # The enum.Enum class itself defines two @DynamicClassAttribute data-descriptors
        # "name" and "value" (which we override in the mocked class for each enum member
        # above). When dealing with inference of an arbitrary instance of the enum
        # class, e.g. in a method defined in the class body like:
        #     class SomeEnum(enum.Enum):
        #         def method(self):
        #             self.name  # <- here
        # In the absence of an enum member called "name" or "value", these attributes
        # should resolve to the descriptor on that particular instance, i.e. enum member.
        # For "value", we have no idea what that should be, but for "name", we at least
        # know that it should be a string, so infer that as a guess.
        if "name" not in target_names:
            code = dedent(
                """
            @property
            def name(self):
                return ''
            """
            )
            name_dynamicclassattr = AstroidBuilder(AstroidManager()).string_build(code)[
                "name"
            ]
            node.locals["name"] = [name_dynamicclassattr]
        break
    return node
Exemplo n.º 14
0
def dict_node(draw, key=const_node(), value=const_node(), **kwargs):
    items = draw(hs.dictionaries(key, value, **kwargs)).items()
    node = nodes.Dict()
    node.postinit(items)
    return node
Exemplo n.º 15
0
def infer_enum_class(node):
    """Specific inference for enums."""
    for basename in node.basenames:
        # TODO: doesn't handle subclasses yet. This implementation
        # is a hack to support enums.
        if basename not in ENUM_BASE_NAMES:
            continue
        if node.root().name == "enum":
            # Skip if the class is directly from enum module.
            break
        dunder_members = {}
        for local, values in node.locals.items():
            if any(not isinstance(value, nodes.AssignName)
                   for value in values):
                continue

            stmt = values[0].statement()
            if isinstance(stmt, nodes.Assign):
                if isinstance(stmt.targets[0], nodes.Tuple):
                    targets = stmt.targets[0].itered()
                else:
                    targets = stmt.targets
            elif isinstance(stmt, nodes.AnnAssign):
                targets = [stmt.target]
            else:
                continue

            inferred_return_value = None
            if isinstance(stmt, nodes.Assign):
                if isinstance(stmt.value, nodes.Const):
                    if isinstance(stmt.value.value, str):
                        inferred_return_value = repr(stmt.value.value)
                    else:
                        inferred_return_value = stmt.value.value
                else:
                    inferred_return_value = stmt.value.as_string()

            new_targets = []
            for target in targets:
                if isinstance(target, nodes.Starred):
                    continue
                # Replace all the assignments with our mocked class.
                classdef = dedent("""
                class {name}({types}):
                    @property
                    def value(self):
                        return {return_value}
                    @property
                    def name(self):
                        return "{name}"
                """.format(
                    name=target.name,
                    types=", ".join(node.basenames),
                    return_value=inferred_return_value,
                ))
                if "IntFlag" in basename:
                    # Alright, we need to add some additional methods.
                    # Unfortunately we still can't infer the resulting objects as
                    # Enum members, but once we'll be able to do that, the following
                    # should result in some nice symbolic execution
                    classdef += INT_FLAG_ADDITION_METHODS.format(
                        name=target.name)

                fake = AstroidBuilder(MANAGER).string_build(classdef)[
                    target.name]
                fake.parent = target.parent
                for method in node.mymethods():
                    fake.locals[method.name] = [method]
                new_targets.append(fake.instantiate_class())
                dunder_members[local] = fake
            node.locals[local] = new_targets
        members = nodes.Dict(parent=node)
        members.postinit([(nodes.Const(k, parent=members),
                           nodes.Name(v.name, parent=members))
                          for k, v in dunder_members.items()])
        node.locals["__members__"] = [members]
        break
    return node
Exemplo n.º 16
0
def transform(cls: nodes.ClassDef):
    """
    Astroid (used by pylint) calls this function on each class definition it discovers.
    cls is an Astroid AST representation of that class.

    Our purpose here is to extract the schema dict from API model classes
    so that we can inform pylint about all of the attributes on those models.
    We do this by injecting attributes on the class for each property in the schema.
    """

    # This is a class which defines attributes in "schema" variable using json schema.
    # Those attributes are then assigned during run time inside the constructor

    # Get the value node for the "schema =" assignment
    schema_dict_node = next(cls.igetattr("schema"))

    extra_schema_properties = {}

    # If the "schema =" assignment's value node is not a simple type (like a dictionary),
    # then pylint cannot infer exactly what it does. Most of the time, this is actually
    # a function call to copy the schema from another class. So, let's find the dictionary.
    if schema_dict_node is astroid.Uninferable:
        # the assignment probably looks like this:
        # schema = copy.deepcopy(ActionAPI.schema)

        # so far we only have the value, but we need the actual assignment
        assigns = [
            n for n in cls.get_children() if isinstance(n, nodes.Assign)
        ]
        schema_assign_name_node = cls.local_attr("schema")[0]
        schema_assign_node = next(
            assign for assign in assigns
            if assign.targets[0] == schema_assign_name_node)
        assigns.remove(schema_assign_node)

        # We only care about "schema = copy.deepcopy(...)"
        schema_dict_node = infer_copy_deepcopy(schema_assign_node.value)
        if not schema_dict_node:
            # This is not an API model class, as it doesn't have
            # something we can resolve to a dictionary.
            return

        # OK, now we need to look for any properties that dynamically modify
        # the dictionary that was just copied from somewhere else.
        # See the note below for why we only care about "properties" here.
        for assign_node in assigns:
            # we're looking for assignments like this:
            # schema["properties"]["ttl"] = {...}
            target = assign_node.targets[0]
            try:
                if (isinstance(target, nodes.Subscript)
                        and target.value.value.name == "schema"
                        and target.value.slice.value.value == "properties"):
                    property_name_node = target.slice.value
                else:
                    # not schema["properties"]
                    continue
            except AttributeError:
                continue

            # schema["properties"]["execution"] = copy.deepcopy(ActionExecutionAPI.schema)
            inferred_value = infer_copy_deepcopy(assign_node.value)

            extra_schema_properties[property_name_node] = (
                inferred_value if inferred_value else assign_node.value)

    if not isinstance(schema_dict_node, nodes.Dict):
        # Not a class we are interested in (like BaseAPI)
        return

    # We only care about "properties" in the schema because that's the only part of the schema
    # that gets translated into dynamic attributes on the model API class.
    properties_dict_node = None
    for key_node, value_node in schema_dict_node.items:
        if key_node.value == "properties":
            properties_dict_node = value_node
            break

    if not properties_dict_node and not extra_schema_properties:
        # Not a class we can do anything with
        return

    # Hooray! We have the schema properties dict now, so we can start processing
    # each property and add an attribute for each one to the API model class node.
    for property_name_node, property_data_node in properties_dict_node.items + list(
            extra_schema_properties.items()):
        property_name = property_name_node.value.replace(
            "-", "_")  # Note: We do the same in Python code

        # Despite the processing above to extract the schema properties dictionary
        # each property in the dictionary might also reference other variables,
        # so we still need to resolve these to figure out each property's type.

        # an indirect reference to copy.deepcopy() as in:
        #   REQUIRED_ATTR_SCHEMAS = {"action": copy.deepcopy(ActionAPI.schema)}
        #   schema = {"properties": {"action": REQUIRED_ATTR_SCHEMAS["action"]}}
        if isinstance(property_data_node, nodes.Subscript):
            var_name = property_data_node.value.name
            subscript = property_data_node.slice.value.value

            # lookup var by name (assume its at module level)
            var_node = next(cls.root().igetattr(var_name))

            # assume it is a dict at this point
            data_node = None
            for key_node, value_node in var_node.items:
                if key_node.value == subscript:
                    # infer will resolve a Dict
                    data_node = next(value_node.infer())
                    if data_node is astroid.Uninferable:
                        data_node = infer_copy_deepcopy(value_node)
                    break
            if data_node:
                property_data_node = data_node

        if not isinstance(property_data_node, nodes.Dict):
            # if infer_copy_deepcopy already ran, we may need to resolve the dict
            data_node = next(property_data_node.infer())
            if data_node is not astroid.Uninferable:
                property_data_node = data_node

        property_type_node = None
        if isinstance(property_data_node, nodes.Dict):
            # We have a property schema, but we only care about the property's type.
            for property_key_node, property_value_node in property_data_node.items:
                if property_key_node.value == "type":
                    property_type_node = next(property_value_node.infer())
                    break

        if property_type_node is None and isinstance(property_data_node,
                                                     nodes.Attribute):
            # reference schema from another file like this:
            #   from ... import TriggerAPI
            #   schema = {"properties": {"trigger": TriggerAPI.schema}}
            # We only pull a schema from another file when it is an "object" (a dict).
            # So, we do not need to do any difficult cross-file processing.
            property_type = "object"
        elif property_type_node is None:
            property_type = None
        elif isinstance(property_type_node, nodes.Const):
            property_type = property_type_node.value
        elif isinstance(property_type_node, (nodes.List, nodes.Tuple)):
            # Hack for attributes with multiple types (e.g. string, null)
            property_type = property_type_node.elts[
                0].value  # elts has "elements" in the list/tuple
        else:
            # We should only hit this if someone has used a different approach
            # for dynamically constructing the property's schema.
            # Expose the AST at this point to facilitate handling that approach.
            raise Exception(property_type_node.repr_tree())

        # Hooray! We've got a property's name at this point.
        # And we have the property's type, if that type was defined in the schema.
        # Now, we can construct the AST node that we'll add to the API model class.

        if property_type == "object":
            node = nodes.Dict()
        elif property_type == "array":
            node = nodes.List()
        elif property_type == "integer":
            node = scoped_nodes.builtin_lookup("int")[1][0]
        elif property_type == "number":
            node = scoped_nodes.builtin_lookup("float")[1][0]
        elif property_type == "string":
            node = scoped_nodes.builtin_lookup("str")[1][0]
        elif property_type == "boolean":
            node = scoped_nodes.builtin_lookup("bool")[1][0]
        elif property_type == "null":
            node = scoped_nodes.builtin_lookup("None")[1][0]
        else:
            # Unknown type
            node = astroid.ClassDef(property_name, None)

        # Create a "property = node" assign node
        assign_node = nodes.Assign(parent=cls)
        assign_name_node = nodes.AssignName(property_name, parent=assign_node)
        assign_node.postinit(targets=[assign_name_node], value=node)

        # Finally, add the property node as an attribute on the class.
        cls.locals[property_name] = [assign_name_node]