Skip to content

fix(common): stop the lazy __getattr__ hiding typos from mypy - #13666

Open
ayaangazali wants to merge 1 commit into
ai-dynamo:mainfrom
ayaangazali:fix/common-getattr-typing
Open

ayaangazali wants to merge 1 commit into
ai-dynamo:mainfrom
ayaangazali:fix/common-getattr-typing

Conversation

@ayaangazali

@ayaangazali ayaangazali commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

dynamo/common/__init__.py and dynamo/common/configuration/__init__.py both resolve their exports through a module-level __getattr__. mypy treats a module that defines __getattr__ as possibly having any attribute, so a typo type-checks clean:

from dynamo.common import config_dumps          # note the s
from dynamo.common.configuration import ArgGroups

The part that makes this worth fixing is that the widening is not local to these files. It applies in every module that imports from these packages, and dynamo.common is imported across the tree.

The fix is to declare the real exports under TYPE_CHECKING and put __getattr__ behind if not TYPE_CHECKING. The checker then sees exactly the names each package provides; the runtime keeps resolving them lazily, unchanged.

This is the same correction @GuanLuo asked for on #13626, applied to the two other packages in dynamo/common that have the pattern. It is deliberately scoped to those two so it stays in one owner area.

Three more packages have the same shape and are not touched here, because they belong to other areas and I did not want to make this a four-area review. Measured the same way, all three currently accept an unknown name:

  • dynamo/sglang/request_handlers/__init__.py
  • dynamo/planner/plugins/transport/__init__.py
  • dynamo/vllm/envs.py

envs.py is the interesting one: it already declares DYN_FORWARDPASS_METRIC_PORT under TYPE_CHECKING, so half the pattern is there and only the guard is missing. Happy to follow up on all three, together or separately, whichever suits the owners.

Validation

Checked with a control, since "mypy reports no error" can mean the module was never really analysed. Modules with no __getattr__ were confirmed to reject an unknown name first, so the probe is known to be capable of failing:

module before after
dynamo.common no error has no attribute "ZZZDoesNotExist" [attr-defined]
dynamo.common.configuration no error has no attribute "ZZZDoesNotExist" [attr-defined]
dynamo.common.multimodal (control, no __getattr__) error error
dynamo.planner.core.budget (control, no __getattr__) error error

Real names still resolve, and with precise types rather than Any:

reveal_type(ArgGroup)       -> def () -> dynamo.common.configuration.arg_group.ArgGroup
reveal_type(env_or_default) -> def [T] (env_var: str, default: T, value_type: ... | None =) -> T
reveal_type(utils)          -> types.ModuleType

Runtime behaviour is unchanged, verified rather than argued:

  • After import dynamo.common, none of config_dump, constants, snapshot, utils is in sys.modules. Laziness is preserved.
  • Touching dynamo.common.utils then loads it and caches it into module globals.
  • ArgGroup, ConfigBase and env_or_default all resolve from dynamo.common.configuration.
  • An unknown attribute still raises AttributeError at runtime on both packages.
  • __all__ is unchanged on both.

Test runs, with the same numbers on clean main with the change stashed, so the failures below are pre-existing and unrelated:

  • pytest components/src/dynamo/common/tests/ — 488 passed, 14 failed, 6 skipped, identical before and after. The 14 are test_video_utils.py codec tests.
  • pytest components/src/dynamo/planner/tests/unit — 608 passed, 1 failed, 3 skipped, identical before and after, excluding three modules that fail to collect on any tree here for missing optional deps (pmdarima and friends).
  • mypy clean on both changed files, pre-commit run --files passes with zero failing hooks.

Open in Devin Review

Summary by CodeRabbit

  • Refactor
    • Improved development-time type checking for lazily loaded configuration and utility components.
    • Preserved existing runtime behavior, including on-demand loading and error handling.

`dynamo/common/__init__.py` and `dynamo/common/configuration/__init__.py` both
resolve their exports through a module-level `__getattr__`. mypy treats a module
with `__getattr__` as possibly having any attribute, so a typo such as

    from dynamo.common import config_dumps        # note the s
    from dynamo.common.configuration import ArgGroups

type-checks clean, in every module that imports from these packages rather than
only in the package itself. `dynamo.common` is imported across the tree, so the
lost typo detection is not local.

Declare the real exports under `TYPE_CHECKING` and put the `__getattr__` behind
`if not TYPE_CHECKING`. The checker then sees exactly the names each package
actually provides, and the runtime keeps resolving them lazily as before.

Verified against a control rather than assumed: modules with no `__getattr__`
(`dynamo.common.multimodal`, `dynamo.planner.core.budget`) already reject an
unknown name, which is what these two now do too.

No runtime change. Importing `dynamo.common` still pulls in none of
`config_dump`, `constants`, `snapshot` or `utils` until one is touched, the
lazy import still caches into module globals, `__all__` is unchanged, and an
unknown attribute still raises `AttributeError`.

Signed-off-by: ayaangazali <ayaangazali.work@gmail.com>
@ayaangazali
ayaangazali requested a review from a team as a code owner August 22, 2026 00:12
Copilot AI lite review requested due to automatic review settings August 22, 2026 00:12
@copy-pr-bot

copy-pr-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ayaangazali
ayaangazali temporarily deployed to external_collaborator August 22, 2026 00:12 — with GitHub Actions Inactive
@ayaangazali
ayaangazali temporarily deployed to external_collaborator August 22, 2026 00:12 — with GitHub Actions Inactive

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi ayaangazali! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added fix external-contribution Pull request is from an external contributor labels Aug 22, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1402e9d4-f18c-4c0a-8fb0-553eca6da965

📥 Commits

Reviewing files that changed from the base of the PR and between e39e021 and fc58af2.

📒 Files selected for processing (2)
  • components/src/dynamo/common/__init__.py
  • components/src/dynamo/common/configuration/__init__.py

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


Walkthrough

The package initializers now declare lazy exports during type checking. Runtime __getattr__ functions remain active only outside type checking and preserve lazy imports, caching, and AttributeError behavior.

Changes

Lazy module typing

Layer / File(s) Summary
Type-checking declarations and lazy resolution
components/src/dynamo/common/__init__.py, components/src/dynamo/common/configuration/__init__.py
The initializers add type-checking-only declarations for lazy modules, classes, and utilities. Runtime __getattr__ functions remain guarded and preserve lazy resolution behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to fc58a

This change narrows static type checking for two package exports while preserving runtime lazy loading and existing behavior; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change and validation, but it omits the required Related Issues section and the requested reviewer-start section. Add the required Related Issues section with an issue reference or confirmation that no issue exists, and identify where reviewers should start.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: preventing lazy getattr from hiding typos from mypy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@ayaangazali

Copy link
Copy Markdown
Contributor Author

Re-checked on current main (d7f06b591f) rather than bumping it. Still reproduces, and here is the before/after rather than a description of it.

Both modules still define an unguarded module-level __getattr__:

components/src/dynamo/common/__init__.py:32               def __getattr__(name: str) -> ModuleType:
components/src/dynamo/common/configuration/__init__.py:24 def __getattr__(name: str):

Probe:

from dynamo.common import DoesNotExistAtAll
from dynamo.common.configuration import AlsoNotReal

On main, mypy reports nothing. Both typos type-check clean, for every module that imports from these packages. With this PR applied, on the same probe:

error: Module "dynamo.common" has no attribute "DoesNotExistAtAll"  [attr-defined]
error: Module "dynamo.common.configuration" has no attribute "AlsoNotReal"  [attr-defined]

One note on reproducing it, because my first attempt did not and it would be easy to conclude there is nothing here. Running mypy against the installed package returns Skipping analyzing "dynamo.common": module is installed, but missing library stubs or py.typed marker, which stops the analysis before the __getattr__ matters. Point it at the source instead:

MYPYPATH=components/src mypy --explicit-package-bases --namespace-packages probe.py

Nothing pushed, diff unchanged since it was opened, still MERGEABLE with no failing checks.

This branch was previously deployed

1 inactive deployment
external_collaborator — fc58af2d Deployed Aug 22, 2026 by ayaangazali via ok-to-test #11791
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants