Skip to content

Exclude old-style imports when they will be replaced with subpackage imports#6294

Merged
masenf merged 2 commits intomainfrom
masenf/pyi-generator-ext
Apr 7, 2026
Merged

Exclude old-style imports when they will be replaced with subpackage imports#6294
masenf merged 2 commits intomainfrom
masenf/pyi-generator-ext

Conversation

@masenf
Copy link
Copy Markdown
Collaborator

@masenf masenf commented Apr 7, 2026

Includes escape hatch for packages outside of the reflex repo to generate init.py stubs without assuming "components" is "reflex_components_core"

…imports

Escape hatch for packages outside of the reflex repo to generate __init__.py
stubs without assuming "components" is "reflex_components_core"
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 7, 2026

Merging this PR will not alter performance

✅ 9 untouched benchmarks


Comparing masenf/pyi-generator-ext (e555de6) with main (f6fab94)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 7, 2026

Greptile Summary

This PR adds an EXCLUDED_IMPORTS mapping and filters old-style reflex.* import aliases out of .pyi stubs that are being regenerated with the new reflex_base.* / reflex_components_core.* paths. It also introduces an is_reflex_package flag so that the component-subpackage rewriting logic in _rewrite_component_import and _get_init_lazy_imports only applies when processing the reflex package itself, providing the promised escape hatch for external packages.

  • New EXCLUDED_IMPORTS dict mirrors DEFAULT_IMPORTS but with pre-0.9 reflex.* source paths; any of these symbols found in a processed file are stripped because DEFAULT_IMPORTS already re-injects them from the new locations.
  • visit_Import filter iterates over the import's names list, removes excluded aliases, and drops the entire node if it becomes empty.
  • is_reflex_package flag is derived from mod.__name__ in _get_init_lazy_imports and threaded through to _rewrite_component_import, preventing components-namespace rewriting for non-reflex packages.
  • Type annotation narrowing in _get_init_lazy_imports: the parameter type was corrected from tuple | ModuleType to ModuleType, which is correct since .___name__ is accessed unconditionally.

Two issues are worth addressing: (1) the first import a file sees bypasses the exclusion check due to the inserted_imports guard ordering, which can leave duplicate symbols in generated stubs; (2) the components.* subpath lookup in _rewrite_component_import is not yet guarded by is_reflex_package, making it inconsistent with the intent of the PR."

Confidence Score: 4/5

Mostly safe, but the first-import ordering bug can produce duplicate symbols in generated .pyi stubs.

One P1 logic issue (excluded imports bypassed for the first import) warrants a 4/5 — the generated stubs could end up with duplicate EventHandler/Var/etc. definitions in files whose first real import is one of the excluded pre-0.9 paths. The P2 is a consistency nit that is unlikely to bite in practice but should be addressed for correctness of the external-package escape hatch.

packages/reflex-base/src/reflex_base/utils/pyi_generator.py — specifically visit_Import (inserted_imports guard ordering) and _rewrite_component_import (unguarded startswith branch).

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/utils/pyi_generator.py Adds EXCLUDED_IMPORTS filtering and is_reflex_package guard; first-import path bypasses the exclusion logic, and components.* subpath lookup is inconsistently unguarded.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[visit_Import called] --> B{inserted_imports?}
    B -- False --> C[Set inserted_imports=True]
    C --> D[Generate default_imports]
    D --> E[Return default_imports + node]
    E --> Z[⚠️ Exclusion check SKIPPED for first import]
    B -- True --> F{node is ImportFrom
and module in EXCLUDED_IMPORTS?}
    F -- Yes --> G[Filter node.names]
    G --> H{node.names empty?}
    H -- Yes --> I[Return empty list
drop import]
    H -- No --> J[Return filtered node]
    F -- No --> K[Return node unchanged]

    subgraph _rewrite_component_import
        R1{is_reflex_package
AND module == components} -- Yes --> R2[Return reflex_components_core]
        R1 -- No --> R3{module.startswith
components. ⚠️ no is_reflex_package guard}
        R3 -- Yes --> R4[Lookup in _COMPONENT_SUBPACKAGE_TARGETS]
        R4 -- Found --> R5[Return absolute target]
        R4 -- Not found --> R6[Return .module]
        R3 -- No --> R6
    end
Loading

Comments Outside Diff (1)

  1. packages/reflex-base/src/reflex_base/utils/pyi_generator.py, line 1091-1104 (link)

    P1 First import bypasses EXCLUDED_IMPORTS check

    The exclusion logic sits in the else branch of the if not self.inserted_imports: guard. The very first non-__future__ import node the visitor sees takes the [*default_imports, node] path and is returned as-is, completely skipping the new filtering block.

    If a file's first real import is, for example,

    from reflex.event import EventHandler

    it will be emitted verbatim, duplicating the EventHandler that DEFAULT_IMPORTS already injects via reflex_base.event. The resulting .pyi stub would contain two EventHandler definitions.

    A safe fix is to run the exclusion filter before deciding whether to prepend the default imports — or to filter node in-place before the early return:

Reviews (1): Last reviewed commit: "Exclude old-style imports when they will..." | Re-trigger Greptile

Comment on lines 1367 to 1381
@@ -1357,7 +1381,7 @@ def _rewrite_component_import(module: str) -> str:
return f".{module}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 components.* subpackage lookup not guarded by is_reflex_package

The PR correctly guards the module == "components" branch with is_reflex_package, but the immediately-following if module.startswith("components."): branch on line 1371 is left unguarded. If a non-reflex package's _SUBMOD_ATTRS happens to contain a key like "components.recharts" (coincidentally matching an entry in _COMPONENT_SUBPACKAGE_TARGETS), it would be silently rewritten to "reflex_components_recharts", producing a broken import for that package.

For consistency, the same guard should wrap the startswith branch:

Suggested change
if is_reflex_package and module == "components":
# "components": ["el", "radix", ...] — these are re-exported submodules.
# Can't map to a single package, but the pyi generator handles each attr individually.
return "reflex_components_core"
if is_reflex_package and module.startswith("components."):
rest = module[len("components."):]
# Try progressively deeper matches (e.g. "datadisplay.code" before "datadisplay").
parts = rest.split(".")
for depth in range(min(len(parts), 2), 0, -1):
key = ".".join(parts[:depth])
target = _COMPONENT_SUBPACKAGE_TARGETS.get(key)
if target is not None:
remainder = ".".join(parts[depth:])
return f"{target}.{remainder}" if remainder else target
return f".{module}"

adhami3310
adhami3310 previously approved these changes Apr 7, 2026
* better visit_Import handling to avoid duplicate imports and missing default imports
* add is_reflex_package escape hatch to second condition in _rewrite_component_import
@masenf masenf merged commit 046e66e into main Apr 7, 2026
39 of 40 checks passed
@masenf masenf deleted the masenf/pyi-generator-ext branch April 7, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants