Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
ENH: ensure that dir(astropy) lists subpackages
  • Loading branch information
mhvk committed Jan 3, 2025
commit 06833cb1f562b6e34d5f8fa89f9c9bbb6543f369
68 changes: 50 additions & 18 deletions astropy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,54 @@

from astropy.utils.system_info import system_info

__all__ = [ # noqa: RUF100, RUF022
"__version__",
"__bibtex__",
# Subpackages (mostly lazy-loaded)
"config",
"constants",
"convolution",
"coordinates",
"cosmology",
"io",
"modeling",
"nddata",
"samp",
"stats",
"table",
"time",
"timeseries",
"uncertainty",
"units",
"utils",
"visualization",
"wcs",
# Functions
"test",
"log",
"find_api_page",
"online_help",
"online_docs_root",
"conf",
"physical_constants",
"astronomical_constants",
"system_info",
]


def __getattr__(attr):
if attr in __all__:
from importlib import import_module

return import_module("astropy." + attr)

raise AttributeError(f"module 'astropy' has no attribute {attr!r}")
Comment thread
mhvk marked this conversation as resolved.
Resolved


def __dir__():
return sorted(set(globals()).union(__all__))


from .version import version as __version__

# The location of the online documentation for astropy
Expand Down Expand Up @@ -198,30 +246,14 @@ def online_help(query):
webbrowser.open(url)


__dir_inc__ = [
"__version__",
"__githash__",

@mhvk mhvk Jan 2, 2025 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had to remove this one since it is not actually defined and ipython seems to use __all__ when doing tab completion.

EDIT: indeed, this means that once you tab-complete, all subpackages are loaded. I think that's OK, since that's just interactive usage anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably okay to remove. It was originally __dir__ back in #4020 but changed to __dir_inc__ in #7617 .

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.

this means that once you tab-complete, all subpackages are loaded.

I think that's how the IPython REPL implements autocompletes for import statements, but I'd like to try it with Python 3.13's new REPL too.

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.

Python 3.13's REPL (aka PyREPL) doesn't seem to even try autocompletion in this area, so no change there.
I agree that it's fine that IPython users will now implicitly load everything if they use , that is part of the IPython deal anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just check and I do get python 3.13.1 to auto-complete astropy.<TAB> - and it also autoloads them to show what is available. Indeed, it does just the logical thing: if I do astropy.sta<TAB> then it finds stats but astropy.time is not loaded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, indeed, confirmed that that does not work on python 3.13 (but does on ipython); I've noticed before that __getattr__/__dir__ don't quite work as nicely with from ... import ... - must involve a slightly different path or so.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

python/cpython#129329 was just merged though I have no idea what Python versions it will land on. Anything we need to follow-up on still?

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.

It'll be in 3.14. We can test this further then, but I don't anticipate any follow up to be needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the info! I opened #18055 as reminder, just in case.

"__bibtex__",
"test",
"log",
"find_api_page",
"online_help",
"online_docs_root",
"conf",
"physical_constants",
"astronomical_constants",
"system_info",
]


from types import ModuleType as __module_type__

# Clean up top-level namespace--delete everything that isn't in __dir_inc__
# Clean up top-level namespace--delete everything that isn't in __all__
# or is a magic attribute, and that isn't a submodule of this package
for varname in dir():
if not (
(varname.startswith("__") and varname.endswith("__"))
or varname in __dir_inc__
or varname in __all__
or (
varname[0] != "_"
and isinstance(locals()[varname], __module_type__)
Expand Down
26 changes: 26 additions & 0 deletions astropy/tests/tests/test_imports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst

import pkgutil
import subprocess
import sys
from textwrap import dedent
from types import ModuleType

import pytest

import astropy


def test_imports():
Expand Down Expand Up @@ -32,3 +40,21 @@ def test_toplevel_namespace():
assert "log" in d
assert "test" in d
assert "sys" not in d


def test_toplevel_lazy_imports():
# Check that subpackages are loaded on demand.
cmd = dedent("""
import astropy, sys
assert 'astropy.units' not in sys.modules
astropy.units
assert 'astropy.units' in sys.modules
""")
cp = subprocess.check_call([sys.executable, "-c", cmd])
assert cp == 0
Comment on lines +46 to +55

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.

@mhvk @pllim looking for other sources of test pollution locally, I hit (I assume by chance) a failure mode for this test. It is extremely puzzling to me that this test might fail in a subprocess, let alone have anything to do with pollution, but unfortunately it is also very hard to debug; I don't even have a good traceback to work with. All I have is

subprocess.CalledProcessError: Command '['/Users/clm/dev/astropy-project/coordinated/astropy/.venv/bin/python3', '-c', "\nimport astropy, sys\nassert 'astropy.units' not in sys.modules\nastropy.units\nassert 'astropy.units' in sys.modules\n"]' returned non-zero exit status 1.

So, I'm pinging you guys in the hope that we can figure out a different test for this feature, but I for one don't have a clear idea how. I tried using monkeypatch.delitem(sys.modules, "astropy.units", raising=False), but it doesn't suffice to simulate "astropy.units has not been imported yet" and the second assert statement fails.

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.

Actually, this should have its own issue. I'll open one now.



def test_toplevel_attribute_error():
# Ensure that our __getattr__ does not leak an import error or so.
with pytest.raises(AttributeError, match="module 'astropy' has no"):
Comment thread
mhvk marked this conversation as resolved.
Resolved
astropy.nonsense
4 changes: 4 additions & 0 deletions docs/changes/17598.other.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
After ``import astropy``, ``dir(astropy)`` will now list all subpackages,
including those that have not yet been loaded. This also means tab
completion will work as expected (e.g., ``from astropy.coo<TAB>`` will
expand to ``from astropy.coordinates``).