Skip to content

API: Allow dir(astropy) to list all subpackage names #17589

Closed

Description

@pllim

What is the problem this feature will solve?

When one is offline and unable to check online docs, one wishes to use dir(astropy) to list all available subpackages for discovery.

Describe the desired outcome

dir(astropy) to list things like "stats".

As of astropy 7.1.dev, it only listed the following:

['__bibtex__',
 '__builtins__',
 '__cached__',
 '__citation__',
 '__dir_inc__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '__version__',
 'astronomical_constants',
 'conf',
 'config',
 'extern',
 'find_api_page',
 'log',
 'logger',
 'online_docs_root',
 'online_help',
 'physical_constants',
 'system_info',
 'test',
 'tests',
 'utils',
 'version']

Additional context

Activity

  1. neutrinoceros commented on Jan 1, 2025

    @neutrinoceros
    Contributor

    The desired outcome is relatively straightfoward to achieve using a module-level __dir__:

    diff --git a/astropy/__init__.py b/astropy/__init__.py
    index a7f600ba4c..a8c8847b99 100644
    --- a/astropy/__init__.py
    +++ b/astropy/__init__.py
    @@ -237,3 +238,11 @@ for varname in dir():
             del locals()[varname]
     
     del varname, __module_type__
    +
    +def __dir__():
    +    local_dir = list(globals())
    +    force_includes = ["stats"]
    +    for incl in force_includes:
    +        if incl not in local_dir:
    +            local_dir.append(incl)
    +    return local_dir

    However, listing, e.g., stats in this fashion would imply that the following works:

    python -c "import astropy; astropy.stats" 
    

    but it doesn't, because stats is only loaded lazily (by design)

    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    AttributeError: module 'astropy' has no attribute 'stats'
    

    Note that

    python -c "import astropy.stats; print(f'''{'stats' in dir(astropy) = }''')"
    

    prints, as of astropy 7.0.0

    'stats' in dir(astropy) = True
    

    My conclusion is I don't think we can satisfy the expectation here without a serious undermining of our lazy loading strategy.

  2. mhvk commented on Jan 2, 2025

    @mhvk
    Contributor

    Can we not get the best of all worlds by also defining a module level __getattr__? Like,

    def __getattr__(attr):
        from importlib import import_module
        try:
            return import_module("astropy." + attr)
        except ImportError:
            pass
        raise AttributeError(...)
    
  3. celik-muhammed commented on Jan 2, 2025

    @celik-muhammed
    dir(module) will return a list of names found in the module’s globals() 
    i.e., everything that is part of the module’s namespace.
    It also relies on __dir__() if defined.
    
    To customize the behavior of dir() for a module, we need to define __dir__().
    
    For example, if we define __dir__() as:
    def __dir__():
        return list(globals().keys()) + _available_submodules
        
    This could cause re-importing. _available_submodules is dynamically generated and returned as a plain value when accessed, but there is no caching or state to remember its previous value.
    
    To avoid this, we need to explicitly manage the state by checking globals() first, 
    then __all__, followed by __dir__(), and finally using __getattr__() for dynamic imports.
    
    This approach reduces complexity and ensures proper handling of dynamically generated submodules and attributes.
    
    
    here full exmple for define seperately __all__, and dynamic namespases _available_submodules 
    
    but my opinion all modules must add globals() then can listed in __all__  list not seperately to easy manage.
    
    
    # Define _available_submodules dynamically (example)
    _available_submodules = ['submodule1', 'submodule2']  # This can be dynamically generated as needed.
    
    # Define __all__ to explicitly list the submodules you want to expose
    __all__ = ['submodule1', 'submodule2']
    
    def __dir__():
        """
        Customize the behavior of dir() for this module. Returns a list of 
        attributes, including both predefined attributes in globals() and 
        dynamically generated submodules.
        """
        # Return predefined attributes and the list of available submodules
        return list(globals().keys()) + _available_submodules
    
    import importlib
    import sys
    import difflib
    def __getattr__(name):
        """
        Dynamically handle undefined attributes in the module. This function 
        attempts to dynamically import submodules or access attributes from 
        globals() and sys.modules.
        """
        
        # Check if the requested name is already in the global namespace
        if name in globals():
            return globals()[name]
    
        # Check if the submodule is already imported (in sys.modules)
        if name in sys.modules:
            return sys.modules[name]
    
        # Check if the requested name exists in the list of available submodules
        if name in _available_submodules:
            try:
                # Dynamically import the submodule
                module = importlib.import_module(f'.{name}', package=__name__)
                # Add to globals and sys.modules for future access
                globals()[name] = module
                sys.modules[name] = module
                return module
            except ModuleNotFoundError:
                pass  # If the module cannot be found, fall through to error handling
    
        # Generate suggestions for mistyped names based on globals() and __all__
        all_names = set(globals().keys()) | set(getattr(__name__, '__all__', []))
        suggestions = difflib.get_close_matches(name, all_names)
        suggestion_msg = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
    
        # Raise an error if the attribute cannot be found
        raise AttributeError(f"Module '{__name__}' has no attribute '{name}'.{suggestion_msg}")
    
    
    
    
  4. mhvk commented on Jan 2, 2025

    @mhvk
    Contributor

    See #17598 - it is a bit simpler than suggested above, since __getattr__ only gets called if a module is not already imported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions