def get_module_attributes(path):
    """
    Get the attributes from the module from the ast.  We use ast because this
    way we can examine modules even if we do not have their required python
    modules installed.
    """
    names = {}
    attributes = OrderedDict()
    python2_names = {"True": True, "False": False, "None": None}

    def get_values(source, dest, extra=None):
        """
        Get a list of the configuration parameters and their defaults.
        """

        def get_value(value):
            """
            Get the actual value from the ast allowing recursion
            """
            class_name = value.__class__.__name__
            # if this is a literal then get its value
            value_type = AST_LITERAL_TYPES.get(class_name)
            if value_type:
                attr_value = getattr(value, value_type)
            else:
                if class_name == "Dict":
                    attr_value = {
                        get_value(k): get_value(v)
                        for k, v in zip(value.keys, value.values)
                    }
                elif class_name == "List":
                    attr_value = [get_value(e) for e in value.elts]
                elif class_name == "Name":
                    # in python 2 True, False, None are Names rather than
                    # NameConstant so we use them for the default
                    default = python2_names.get(value.id)
                    attr_value = extra.get(value.id, default)
                elif class_name == "Tuple":
                    attr_value = tuple(get_value(e) for e in value.elts)
                elif class_name == "UnaryOp":
                    op = value.op.__class__.__name__
                    attr_value = get_value(value.operand)
                    # we only understand negation
                    if op == "USub":
                        attr_value = -attr_value
                    else:
                        attr_value = f"UNKNOWN {class_name} UnaryOp {op}"
                elif class_name == "BinOp":
                    left = get_value(value.left)
                    right = get_value(value.right)
                    op = value.op.__class__.__name__
                    try:
                        if op == "Add":
                            attr_value = left + right
                        elif op == "Mult":
                            attr_value = left * right
                        else:
                            attr_value = f"UNKNOWN {class_name} BinOp {op}"
                    except Exception:
                        attr_value = f"UNKNOWN {class_name} BinOp error"
                else:
                    attr_value = f"UNKNOWN {class_name}"
            return attr_value

        if extra is None:
            extra = {}

        for line in source:
            if isinstance(line, ast.Assign):
                if len(line.targets) == 1 and isinstance(line.targets[0], ast.Name):
                    attr = line.targets[0].id
                    value = line.value
                    dest[attr] = get_value(value)

    with path.open() as f:
        tree = ast.parse(f.read(), "")
        # some modules have constants defined we need to find these
        get_values(tree.body, names)
        for statement in tree.body:
            # look for the Py3status class and find it's attributes
            if isinstance(statement, ast.ClassDef) and statement.name == "Py3status":
                get_values(statement.body, attributes, names)
    return attributes

    Get the attributes from the module from the ast.  We use ast because this
    way we can examine modules even if we do not have their required python
    modules installed.
    
tests/test_module_doc.py