Repository: Textualize/textual

Issue:
ImportError: Package 'textual.widgets' has no class 'agm'
Have you checked closed issues? https://github.com/Textualize/textual/issues?q=is%3Aissue+is%3Aclosed -> *yes*

I want to profile a FastAPI based application, and use `memray run -o memory.log /usr/local/bin/uvicorn ...` to run the application.
However, memray stops by throwing the following exception:
```
root@8593ec016ab9:/app# memray run -o memory.log /usr/local/bin/uvicorn app.main:app --host 0.0.0.0 --port 80
Writing profile results into memory.log
Memray WARNING: Correcting symbol for aligned_alloc from 0x7fe57d857d90 to 0x7fe57e0de550
[memray] Successfully generated profile results.

You can now generate reports from the stored allocation records.
Some example commands to generate reports:

/usr/local/bin/python -m memray flamegraph memory.log
Traceback (most recent call last):
  File "/usr/local/bin/memray", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.9/site-packages/memray/commands/__init__.py", line 138, in main
    arg_values.entrypoint(arg_values, parser)
  File "/usr/local/lib/python3.9/site-packages/memray/commands/run.py", line 339, in run
    _run_with_file_output(args)
  File "/usr/local/lib/python3.9/site-packages/memray/commands/run.py", line 188, in _run_with_file_output
    _run_tracker(
  File "/usr/local/lib/python3.9/site-packages/memray/commands/run.py", line 77, in _run_tracker
    runpy.run_path(args.script, run_name="__main__")
  File "/usr/local/lib/python3.9/runpy.py", line 288, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "/usr/local/lib/python3.9/runpy.py", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/usr/local/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/usr/local/bin/uvicorn", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.9/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/uvicorn/main.py", line 409, in main
    run(
  File "/usr/local/lib/python3.9/site-packages/uvicorn/main.py", line 575, in run
    server.run()
  File "/usr/local/lib/python3.9/site-packages/uvicorn/server.py", line 65, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/usr/local/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.9/site-packages/uvicorn/server.py", line 69, in serve
    await self._serve(sockets)
  File "/usr/local/lib/python3.9/site-packages/uvicorn/server.py", line 76, in _serve
    config.load()
  File "/usr/local/lib/python3.9/site-packages/uvicorn/config.py", line 433, in load
    self.loaded_app = import_from_string(self.app)
  File "/usr/local/lib/python3.9/site-packages/uvicorn/importer.py", line 19, in import_from_string
    module = importlib.import_module(module_str)
  File "/usr/local/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/app/simulation/app/main.py", line 8, in <module>
    import skops.io
  File "/usr/local/lib/python3.9/site-packages/skops/io/__init__.py", line 1, in <module>
    from ._persist import dump, dumps, get_untrusted_types, load, loads
  File "/usr/local/lib/python3.9/site-packages/skops/io/_persist.py", line 22, in <module>
    module = importlib.import_module(module_name, package="skops.io")
  File "/usr/local/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/local/lib/python3.9/site-packages/skops/io/_general.py", line 16, in <module>
    from ._trusted_types import (
  File "/usr/local/lib/python3.9/site-packages/skops/io/_trusted_types.py", line 17, in <module>
    SCIPY_UFUNC_TYPE_NAMES = get_public_type_names(module=scipy.special, oftype=np.ufunc)
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 230, in get_public_type_names
    {
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 234, in <setcomp>
    and (type_name := get_type_name(obj)).startswith(module_name)
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 179, in get_type_name
    return f"{get_module(t)}.{t.__name__}"
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 86, in get_module
    return whichmodule(obj, obj.__name__)
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 49, in whichmodule
    if _getattribute(module, name)[0] is obj:
  File "/usr/local/lib/python3.9/site-packages/skops/io/_utils.py", line 24, in _getattribute
    obj = getattr(obj, subpath)
  File "/usr/local/lib/python3.9/site-packages/textual/widgets/__init__.py", line 107, in __getattr__
    raise ImportError(f"Package 'textual.widgets' has no class '{widget_class}'")
ImportError: Package 'textual.widgets' has no class 'agm'
```
I do not use the package `textual`, but the package `skops.io` seems to get a hold on `agm`, which it can not import.
I reported the issue on the Memray project [(Issue)](https://github.com/bloomberg/memray/issues/688), but got the response that it is an textual issue. However, running the FastAPI application does not lead to this issue.

```
root@8593ec016ab9:/app# pip install textual
Requirement already satisfied: textual in /usr/local/lib/python3.9/site-packages (0.81.0)
Requirement already satisfied: rich>=13.3.3 in /usr/local/lib/python3.9/site-packages (from textual) (13.9.0)
Requirement already satisfied: typing-extensions<5.0.0,>=4.4.0 in /usr/local/lib/python3.9/site-packages (from textual) (4.12.2)
Requirement already satisfied: platformdirs<5,>=3.6.0 in /usr/local/lib/python3.9/site-packages (from textual) (4.3.6)
Requirement already satisfied: markdown-it-py[linkify,plugins]>=2.1.0 in /usr/local/lib/python3.9/site-packages (from textual) (3.0.0)
Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.9/site-packages (from markdown-it-py[linkify,plugins]>=2.1.0->textual) (0.1.2)
Requirement already satisfied: mdit-py-plugins in /usr/local/lib/python3.9/site-packages (from markdown-it-py[linkify,plugins]>=2.1.0->textual) (0.4.2)
Requirement already satisfied: linkify-it-py<3,>=1 in /usr/local/lib/python3.9/site-packages (from markdown-it-py[linkify,plugins]>=2.1.0->textual) (2.0.3)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.9/site-packages (from rich>=13.3.3->textual) (2.18.0)
Requirement already satisfied: uc-micro-py in /usr/local/lib/python3.9/site-packages (from linkify-it-py<3,>=1->markdown-it-py[linkify,plugins]>=2.1.0->textual) (1.0.3)
```

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.81.0 |
| Rich    | 13.9.0 |

## Python

| Name           | Value                 |
|----------------|-----------------------|
| Version        | 3.9.19                |
| Implementation | CPython               |
| Compiler       | GCC 12.2.0            |
| Executable     | /usr/local/bin/python |

## Operating System

| Name    | Value                                 |
|---------|---------------------------------------|
| System  | Linux                                 |
| Release | 5.10.0-32-cloud-amd64                 |
| Version | #1 SMP Debian 5.10.223-1 (2024-08-10) |

## Terminal

| Name                 | Value     |
|----------------------|-----------|
| Terminal Application | *Unknown* |
| TERM                 | xterm     |
| COLORTERM            | *Not set* |
| FORCE_COLOR          | *Not set* |
| NO_COLOR             | *Not set* |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=298, height=47 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 298                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 47                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

Hints from issue comments:
@github-actions: We found the following entries in the [FAQ](https://textual.textualize.io/FAQ/) which you may find helpful:


- [How do I center a widget in a screen?](https://textual.textualize.io/FAQ/#how-do-i-center-a-widget-in-a-screen)
- [No widget called TextLog](https://textual.textualize.io/FAQ/#no-widget-called-textlog)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)@godlygeek: This also breaks the Python test suite.

Running the test suite without Memray:

```shell-session
$ python -m test test_list
Using random seed: 3106590115
0:00:00 load avg: 0.28 Run 1 test sequentially
0:00:00 load avg: 0.28 [1/1] test_list

== Tests result: SUCCESS ==

1 test OK.

Total duration: 481 ms
Total tests: run=58
Total test files: run=1/1
Result: SUCCESS
```
Running the test suite with Memray:
```shell-session
$ python -m memray run -m test test_list
Writing profile results into memray-test.23119.bin
Using random seed: 3321430036
0:00:00 load avg: 0.32 Run 1 test sequentially
0:00:00 load avg: 0.32 [1/1] test_list
test test_list crashed -- Traceback (most recent call last):
  File "/opt/bb/lib/python3.11/test/libregrtest/single.py", line 173, in _runtest_env_changed_exc
    clear_caches()
  File "/opt/bb/lib/python3.11/test/libregrtest/utils.py", line 179, in clear_caches
    if hasattr(mod, '__warningregistry__'):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mwoznisk/.pyenv/versions/memray-3.11/lib/python3.11/site-packages/textual/widgets/__init__.py", line 107, in __getattr__
    raise ImportError(f"Package 'textual.widgets' has no class '{widget_class}'")
ImportError: Package 'textual.widgets' has no class '__warningregistry__'

test_list failed (uncaught exception)

== Tests result: FAILURE ==

1 test failed:
    test_list

Total duration: 15 ms
Total tests: run=0
Total test files: run=1/1 failed=1
Result: FAILURE
```@github-actions: Don't forget to [star](https://github.com/Textualize/textual) the repository!

Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates.@ChrisDAT20: Thanks a lot for the quick response and the fix. Already tried it and the issue does not happen anymore!

Solution patch:
diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py
index 7036ca44f8..bcaac621c0 100644
--- a/src/textual/widgets/__init__.py
+++ b/src/textual/widgets/__init__.py
@@ -104,7 +104,7 @@ def __getattr__(widget_class: str) -> type[Widget]:
         pass
 
     if widget_class not in __all__:
-        raise ImportError(f"Package 'textual.widgets' has no class '{widget_class}'")
+        raise AttributeError(f"Package 'textual.widgets' has no class '{widget_class}'")
 
     widget_module_path = f"._{camel_to_snake(widget_class)}"
     module = import_module(widget_module_path, package="textual.widgets")


Test patch:
diff --git a/tests/test_widget.py b/tests/test_widget.py
index d5e79c0c11..22e784b3b2 100644
--- a/tests/test_widget.py
+++ b/tests/test_widget.py
@@ -576,3 +576,21 @@ def test_bad_widget_name_raised() -> None:
 
         class lowercaseWidget(Widget):
             pass
+
+
+def test_lazy_loading() -> None:
+    """Regression test for https://github.com/Textualize/textual/issues/5077
+
+    Check that the lazy loading magic doesn't break attribute access.
+
+    """
+
+    with pytest.raises(ImportError):
+        from textual.widgets import Foo  # nopycln: import
+
+    from textual import widgets
+    from textual.widgets import Label
+
+    assert not hasattr(widgets, "foo")
+    assert not hasattr(widgets, "bar")
+    assert hasattr(widgets, "Label")