Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion tensorrt_llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,17 @@ def __getattr__(name):
entry = _LAZY_ATTRS.get(name)
if entry is not None:
module_name, attr = entry
module = importlib.import_module(module_name)
try:
module = importlib.import_module(module_name)
except AttributeError as e:
# An AttributeError escaping the module is a bug inside it, not a
# missing name here. Raised as is, hasattr() and getattr() with a
# default would hide it, and ``from ... import`` would run the
# module twice (losing the cause unless the name is a submodule).
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"{module_name} raised {e!r}",
name=__name__) from e
value = module if attr is None else getattr(module, attr)
globals()[name] = value # cache: subsequent access skips __getattr__
return value
Expand All @@ -126,6 +136,12 @@ def __getattr__(name):
raise
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}") from None
except AttributeError as e:
# Same as above: a bug inside the submodule, not a missing name.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"{__name__}.{name} raised {e!r}",
name=__name__) from e


def __dir__():
Expand Down
17 changes: 16 additions & 1 deletion tensorrt_llm/_torch/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,22 @@ def __getattr__(name: str):
raise
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}") from None
module = importlib.import_module(f".{module_name}", __name__)
except AttributeError as e:
Comment thread
YihuiLu512 marked this conversation as resolved.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"{__name__}.{name} raised {e!r}",
name=__name__) from e
try:
module = importlib.import_module(f".{module_name}", __name__)
except AttributeError as e:
# An AttributeError escaping the submodule is a bug inside it, not a
# missing name here. Raised as is, hasattr() and ``from ... import``
# would swallow it, and from-import would run the submodule again,
# hiding the cause.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"{__name__}.{module_name} raised {e!r}",
name=__name__) from e
attr = getattr(module, name)
globals()[name] = attr # cache: subsequent access skips __getattr__
return attr
Expand Down
19 changes: 17 additions & 2 deletions tensorrt_llm/_torch/pyexecutor/sampler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,26 @@
_SUBMODULES = frozenset({"sampler", "sampler_common", "sampling_resolution", "ops"})


def _import_submodule(name: str, submodule: str):
try:
return importlib.import_module(f"{__name__}.{submodule}")
except AttributeError as e:
# An AttributeError escaping the submodule is a bug inside it, not a
# missing name here. Raised as is, hasattr() and getattr() with a
# default would hide it, and ``from ... import`` would run the
# submodule twice (losing the cause unless the name is a submodule).
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"{__name__}.{submodule} raised {e!r}",
name=__name__,
) from e


def __getattr__(name: str):
if name in _SUBMODULES:
return importlib.import_module(f"{__name__}.{name}")
return _import_submodule(name, name)
# Lazily forward everything else to the sampler orchestration module.
_sampler = importlib.import_module(f"{__name__}.sampler")
_sampler = _import_submodule(name, "sampler")
try:
return getattr(_sampler, name)
except AttributeError:
Expand Down
12 changes: 11 additions & 1 deletion tensorrt_llm/_torch/visual_gen/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@

def __getattr__(name):
if name in __all__:
from ..attention import attention
try:
from ..attention import attention
except AttributeError as e:
# An AttributeError escaping ``attention`` is a bug inside it, not a
# missing name here. Raised as is, hasattr() and ``from ... import``
# would swallow it, and from-import would run it again, hiding the cause.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing "
f"tensorrt_llm._torch.visual_gen.attention.attention raised {e!r}",
name=__name__,
) from e

return getattr(attention, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19 changes: 18 additions & 1 deletion tensorrt_llm/visual_gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,18 @@
def __getattr__(name):
module_name = _LAZY_ATTRS.get(name)
if module_name is not None:
value = getattr(importlib.import_module(module_name), name)
try:
module = importlib.import_module(module_name)
except AttributeError as e:
# An AttributeError escaping the module is a bug inside it, not a
# missing name here. Raised as is, hasattr() and ``from ... import``
# would swallow it, and from-import would run the module again,
# hiding the cause.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing {module_name} raised {e!r}",
name=__name__,
) from e
value = getattr(module, name)
globals()[name] = value # cache: subsequent access skips __getattr__
return value
# Fall back to plain submodules (tensorrt_llm.visual_gen.sparse_attention,
Expand All @@ -108,6 +119,12 @@ def __getattr__(name):
if e.name != f"{__name__}.{name}":
raise
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
except AttributeError as e:
# Same as above: a bug inside the submodule, not a missing name.
raise ImportError(
f"cannot import {name!r} from {__name__!r}: importing {__name__}.{name} raised {e!r}",
name=__name__,
) from e


def __dir__():
Expand Down
134 changes: 134 additions & 0 deletions tests/unittest/others/test_lazy_getattr_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Guards how the PEP 562 ``__getattr__`` of the lazily loaded packages reports a broken submodule.

An ``AttributeError`` raised while a submodule is being imported is a bug inside that submodule.
``hasattr()``, ``getattr(..., default)`` and ``from package import name`` all treat an
``AttributeError`` from ``__getattr__`` as "no such name", so it must leave ``__getattr__`` as an
``ImportError`` chained to the original error. Otherwise ``hasattr`` returns False,
``getattr(..., default)`` returns the default, and ``from ... import`` imports the submodule a
second time and, for a name that is not itself a submodule, reports ``cannot import name``
without the cause.

The fault is injected with a ``sys.meta_path`` finder that raises when the target submodule is
imported, so it covers both ``importlib.import_module`` and ``from . import`` inside
``__getattr__``.
"""

import importlib
import importlib.abc
import sys

import pytest

pytestmark = pytest.mark.cpu_only

_MARKER = "injected-by-test_lazy_getattr_errors"

# (package, name accessed on it, submodule that the access imports)
CASES = [
("tensorrt_llm", "AutoConfig", "tensorrt_llm.models.automodel"),
("tensorrt_llm", "profiler", "tensorrt_llm.profiler"),
(
"tensorrt_llm._torch.models",
"Starcoder2ForCausalLM",
"tensorrt_llm._torch.models.modeling_starcoder2",
),
(
"tensorrt_llm._torch.models",
"modeling_starcoder2",
"tensorrt_llm._torch.models.modeling_starcoder2",
),
("tensorrt_llm.visual_gen", "VisualGenParams", "tensorrt_llm.visual_gen.params"),
("tensorrt_llm.visual_gen", "sparse_attention", "tensorrt_llm.visual_gen.sparse_attention"),
(
"tensorrt_llm._torch.pyexecutor.sampler",
"TorchSampler",
"tensorrt_llm._torch.pyexecutor.sampler.sampler",
),
(
"tensorrt_llm._torch.pyexecutor.sampler",
"sampler_common",
"tensorrt_llm._torch.pyexecutor.sampler.sampler_common",
),
(
"tensorrt_llm._torch.visual_gen.modules",
"Attention",
"tensorrt_llm._torch.visual_gen.attention.attention",
),
]
_IDS = [f"{pkg}.{name}" for pkg, name, _ in CASES]


class _RaiseOnImport(importlib.abc.MetaPathFinder):
def __init__(self, fullname: str):
self.fullname = fullname
self.attempts = 0

def find_spec(self, name, path=None, target=None):
if name != self.fullname:
return None
self.attempts += 1
raise AttributeError(f"{_MARKER}: importing {name}")


@pytest.fixture
def broken_submodule(monkeypatch):
"""Make importing ``target`` raise AttributeError until the test ends; return (package, finder)."""

def install(pkg_name: str, name: str, target: str):
pkg = importlib.import_module(pkg_name)
parent_name, _, leaf = target.rpartition(".")
parent = importlib.import_module(parent_name)
finder = _RaiseOnImport(target)
monkeypatch.setattr(sys, "meta_path", [finder, *sys.meta_path])
# Edit the namespaces directly: hasattr() (used by monkeypatch.delattr) would call the
# package's __getattr__ and re-import the target before the test starts.
monkeypatch.delitem(sys.modules, target, raising=False)
monkeypatch.delitem(vars(parent), leaf, raising=False)
monkeypatch.delitem(vars(pkg), name, raising=False)
assert target not in sys.modules and leaf not in vars(parent) and name not in vars(pkg)
assert finder.attempts == 0
return pkg, finder

return install


def _assert_chained(excinfo, finder):
assert isinstance(excinfo.value.__cause__, AttributeError), repr(excinfo.value)
assert _MARKER in str(excinfo.value.__cause__)
assert finder.attempts == 1


@pytest.mark.parametrize("pkg_name,name,target", CASES, ids=_IDS)
def test_from_import_reports_cause_once(broken_submodule, pkg_name, name, target):
_, finder = broken_submodule(pkg_name, name, target)
with pytest.raises(ImportError) as excinfo:
exec(f"from {pkg_name} import {name}", {})
_assert_chained(excinfo, finder)


@pytest.mark.parametrize("pkg_name,name,target", CASES, ids=_IDS)
@pytest.mark.parametrize("probe", ["attr", "hasattr", "getattr_default"])
def test_probe_does_not_hide_broken_submodule(broken_submodule, probe, pkg_name, name, target):
pkg, finder = broken_submodule(pkg_name, name, target)
with pytest.raises(ImportError) as excinfo:
if probe == "attr":
getattr(pkg, name)
elif probe == "hasattr":
hasattr(pkg, name)
else:
getattr(pkg, name, None)
_assert_chained(excinfo, finder)
Loading