Skip to content

Circular import of name defined in same module, reported as an error. #12574

@mrolle45

Description

@mrolle45

I have a project, which I've reduced to a minimum which shows the same problem. It has 9 modules, excerpts are:

debugging.py:

from typing import *
if TYPE_CHECKING:
	from .instr_common import InstrCtx
	from .instruct import InstrTab
----------
instr_common.py:

from __future__ import annotations
from .common import *
from . import debugging

class InstrCtx(NamedTuple):
	tab : InstrTab
----------------
instruct.py:

from __future__ import annotations
from .instr_merge import *

The other modules, with the above, form an SCC of imports, so all 9 are analyzed together.

Expected Behavior
solver\debugging.py:12:2: error: Module "solver.instruct" has no attribute "InstrTab"
The InstrTab import is an error because the name does not exist in the imported module.
The class def of InstrCtx should be OK, since the import from common is importing the same thing.

Actual Behavior

solver\debugging.py:11:2: error: Module "solver.instr_common" has no attribute "InstrCtx"
solver\debugging.py:12:2: error: Module "solver.instruct" has no attribute "InstrTab"
solver\instr_common.py:12:1: error: Name "InstrCtx" already defined (possibly by an import)

Discussion
In some cases, a circular import would be a programming error, as the runtime results could vary depending on which of the modules in the SCC is imported first.
In my case, the import in solver.debugging is guarded by TYPE_CHECKING, and so it has no effect at runtime. mypy makes a placeholder for solver.debugging.InstrCtx, then later this is imported into solver.instr_common as a Var object.
Now if the placeholder resulted from a guarded import, then mypy should not consider that InstrCtx is imported at runtime, and so it should be ignored.
In effect, the import of solver.debugging.InstrCtx is private. It is not visible to other modules, and is visible within solver.debugging only for type analysis purposes.

I propose adding the following to semanal.py to make imported names non-public when MYPY-guarded:

in SemanticAnalyzer.visit_import:
			if as_id is not None:
				base_id = id
				imported_id = as_id
				module_public = use_implicit_reexport or id.split(".")[-1] == as_id
			else:
				base_id = id.split('.')[0]
				imported_id = base_id
				module_public = use_implicit_reexport
--->			if i.is_mypy_only: module_public = False

in SemanticAnalyzer.visit_import_all:
					module_public = self.is_stub_file or self.options.implicit_reexport
--->					if i.is_mypy_only: module_public = False

in SemanticAnalyzer.visit_import_from:
			module_public = use_implicit_reexport or (as_id is not None and id == as_id)
--->			if imp.is_mypy_only: module_public = False

Attached is a zip file with:

  • All the project modules.
  • The config file.
  • log.txt from mypy run before making changes.
  • log2.txt from mypy run after making changes.
    Exp3.zip

Your Environment

  • Mypy version used: 0.931
  • Mypy command-line flags: -v
  • Mypy configuration options: see attached
  • Python version used: 3.7
  • Operating system and version: Windows 10

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions