readthedocs / sphinx-autoapi

A new approach to API documentation in Sphinx.
https://sphinx-autoapi.readthedocs.io/
MIT License
415 stars 126 forks source link

Wrong namespace resolution for base clases when using star import #404

Open jorgepiloto opened 9 months ago

jorgepiloto commented 9 months ago

The issue 🐞

Consider the following project:

.
β”œβ”€β”€ package/
β”‚   β”œβ”€β”€__init__.py
β”‚Β Β  β”œβ”€β”€ module_A.py
β”‚Β Β  └── module_B.py
β”œβ”€β”€ conf.py
└── index.rst

The content of each file:

module_A.py module_B.py index.rst conf.py
```python from enum import IntEnum from .module_B import * __all__ = ["FooA"] class FooA(IntEnum): ITEM_0 = 0 ITEM_1 = 1 ITEM_2 = 2 ``` ```python from enum import IntEnum __all__ = ["FooB"] class FooB(IntEnum): ITEM_0 = 0 ITEM_1 = 1 ITEM_2 = 2 ``` ```rst Package docs ============ .. toctree:: autoapi/index Contents: .. toctree:: :maxdepth: 2 ``` ```python templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" project = "package" copyright = "2015, readthedocs" author = "readthedocs" version = "0.1" release = "0.1" language = "en" exclude_patterns = ["_build"] pygments_style = "sphinx" todo_include_todos = False html_theme = "alabaster" htmlhelp_basename = "Package" extensions = ["autoapi.extension"] autoapi_dirs = ["package"] autoapi_file_pattern = "*.py" ```

The result is:

Class A Class B

Investigations πŸ”Ž

It looks like the import matters:

This fails This works This works too
```python from enum import IntEnum from .module_B import * ``` ```python from .module_B import * from enum import IntEnum ``` ```python from enum import IntEnum from .module_B import FooB ```

I am a bit surprised that this behavior applies even if __all__ is used in both modules.

Origin of the problem It looks like the function resolve_qualname is the one causing the issue.

jorgepiloto commented 9 months ago

I was able to come up with some logic to prevent this from happening:

module_node, assigns = lookup_node.lookup(top_level_name)

if len(assigns) == 1 and isinstance(assigns[0], astroid.ImportFrom) and ('*', None) == assigns[0].names[0]:
    import_nodes = [
        import_node
        for import_node in module_node.get_children()
        if is_import_or_importfrom_node(import_node)
    ]
    for import_node in import_nodes:
        if any(top_level_name in namealias for namealias in import_node.names):
            assigns = [import_node]

I must confess I do not like previous code.

Also, it does not work when you run into a double wildcard import...