"""
A utility package containing functions and variables to support development of
the core functionality in `plasmapy_sphinx`.
"""
__all__ = [
"default_grouping_info",
"find_mod_objs",
"get_custom_grouping_info",
"package_dir",
"templates_dir",
]
import inspect
import os
from collections import OrderedDict
from importlib import import_module
from sphinx.application import Sphinx
from typing import Any, Dict
package_dir = os.path.abspath(os.path.dirname(__file__))
"""Absolute path to the `plasmapy_sphinx` package directory."""
templates_dir = os.path.join(package_dir, "templates")
"""Absolute path to the `plasmapy_sphinx` templates directory."""
default_grouping_info = OrderedDict(
{
"modules": {"title": "Sub-Packages & Modules"},
"classes": {"title": "Classes"},
"exceptions": {"title": "Exceptions"},
"warnings": {"title": "Warnings"},
"functions": {"title": "Functions"},
"variables": {"title": "Variables & Attributes"},
},
)
"""
Dictionary containing information related to the default object groups used
by the :rst:dir:`automodapi` and :rst:dir:`automodsumm` directives. Can be
extend using the configuration value :confval:`automodapi_custom_groups`.
"""
[docs]
def get_custom_grouping_info(app: Sphinx):
"""
Retrieve the custom groups dictionary defined by the configuration value
:confval:`automodapi_custom_groups`.
"""
try:
_info = app.config.automodapi_custom_groups
except AttributeError:
_info = {}
return _info
[docs]
def find_mod_objs(modname: str, app: Sphinx = None) -> Dict[str, Dict[str, Any]]:
"""
Inspect the module ``modname`` for all the contained objects, sort for the
object type (module, function, class, etc.), and return a dictionary containing
object names, fully qualified names, and instances.
Parameters
----------
modname : str
Name of the module (e.g. ``"plasmapy_sphinx.utils'``) to be inspect.
app : `~sphinx.application.Sphinx`
Instance of the `Sphinx` application.
Returns
-------
mod_objs : Dict[str, Dict[str, List[Any]]]
A dictionary containing names, qualified names, and objects instances of all
the objects in ``modname`` sorted by their respective group (module, class,
function, etc.)
The first key of the dictionary represents the object type (modules, classes,
functions, etc.). The second key is either ``"names"`` (list of all object
short names), ``"qualnames"`` (list of all object qualified names), and
``"objs"`` (list of object instances).
Examples
--------
>>> find_mod_objs("plasmapy_sphinx.utils")
{
'functions': {
'names': ['find_mod_objs', 'get_custom_grouping_info'],
'qualnames': [
'plasmapy_sphinx.utils.find_mod_objs',
'plasmapy_sphinx.utils.get_custom_grouping_info',
],
'objs': [
<function plasmapy_sphinx.utils.find_mod_objs>,
<function plasmapy_sphinx.utils.get_custom_grouping_info>,
]
},
'variables': {
'names': ['default_grouping_info', 'package_dir', 'templates_dir'],
'qualnames': [
'plasmapy_sphinx.utils.default_grouping_info',
'plasmapy_sphinx.utils.package_dir',
'plasmapy_sphinx.utils.templates_dir',
],
'objs': [
OrderedDict(...),
"/.../plasmapy_sphinx",
"/.../plasmapy_sphinx/templates",
]
}
}
Notes
-----
If the module contains the ``__all__`` dunder, then the routine groups the
objects specified in the dunder; otherwise, it will search the module's `globals`,
minus any private or special members. The routing will then group the
module objects in the following order...
1. Group any imported modules or packages.
- Regardless of if ``__all__`` is defined, the routine will first search
the module's `globals` for any imported modules or packages.
- Any 3rd party modules are excluded unless specified in ``__all__``.
- Any non-direct sub-modules are excluded unless specified in ``__all__``.
2. Custom groups defined by :confval:`automodapi_custom_groups` are then collected.
3. The remaining objects are grouped into the default groupds defined by
:attr:`default_grouping_info`.
"""
if app is not None:
if isinstance(app, Sphinx):
cgroups_def = get_custom_grouping_info(app)
else:
# assuming dict for testing
cgroups_def = app
cgroups = set(cgroups_def)
else:
cgroups_def = {}
cgroups = set()
mod = import_module(modname)
pkg_name = modname.split(".")[0]
# define what to search
pkg_names = {name for name in mod.__dict__.keys() if not name.startswith("_")}
if hasattr(mod, "__all__"):
no_all = False
names_to_search = set(mod.__all__)
else:
no_all = True
names_to_search = pkg_names
# filter pkg_names
for name in pkg_names.copy():
obj = getattr(mod, name)
if not no_all and name in names_to_search:
continue
ismod = inspect.ismodule(obj)
ispkg = ismod and obj.__package__ == obj.__name__
# remove test folders
if ispkg and obj.__package__.split(".")[-1] == "tests":
pkg_names.remove(name)
continue
# remove 3rd party objects
if ismod and obj.__package__.split(".")[0] != pkg_name:
pkg_names.remove(name)
continue
elif (
not ismod
and hasattr(obj, "__module__")
and obj.__module__.split(".")[0] != pkg_name
):
# Note: this will miss ufuncs like numpy.sqrt since they do not have
# a __module__ property
pkg_names.remove(name)
continue
# remove non direct sub-pkgs and mods of modname
if ismod:
if not obj.__name__.startswith(modname):
pkg_names.remove(name)
continue
else:
nm = obj.__name__[len(modname) :].split(".")
nm.remove("")
if len(nm) != 1:
pkg_names.remove(name)
continue
# find local modules first
names_of_modules = set()
for name in pkg_names.copy():
obj = getattr(mod, name)
if inspect.ismodule(obj):
names_of_modules.add(name)
mod_objs = {"modules": {"names": []}}
if len(names_of_modules) > 0:
names_of_modules = names_of_modules
mod_objs["modules"]["names"] = list(names_of_modules)
names_to_search = names_to_search - names_of_modules
# find and filter custom groups
for name in cgroups:
dunder = cgroups_def[name]["dunder"]
if hasattr(mod, dunder):
custom_names = set(getattr(mod, dunder))
else:
continue
if len(custom_names) > 0:
mod_objs.update({name: {"names": list(custom_names)}})
names_to_search = names_to_search - custom_names
# gather all remaining groups
mod_objs.update(
{
"classes": {"names": []},
"exceptions": {"names": []},
"warnings": {"names": []},
"functions": {"names": []},
"variables": {"names": []},
}
)
for name in names_to_search:
obj = getattr(mod, name)
if inspect.isroutine(obj):
# is a user-defined or built-in function
mod_objs["functions"]["names"].append(name)
elif inspect.isclass(obj):
if issubclass(obj, Warning):
mod_objs["warnings"]["names"].append(name)
elif issubclass(obj, BaseException):
mod_objs["exceptions"]["names"].append(name)
else:
mod_objs["classes"]["names"].append(name)
else:
mod_objs["variables"]["names"].append(name)
# retrieve and defined qualnames and objs
for obj_type in list(mod_objs):
if len(mod_objs[obj_type]["names"]) == 0:
del mod_objs[obj_type]
continue
mod_objs[obj_type].update({"qualnames": [], "objs": []})
for name in list(mod_objs[obj_type]["names"]):
# Note: The 'qualname' is always constructed with 'name' so when
# something like
#
# def func(...):
# ...
#
# f2 = func
#
# is done, then the 'qualname' ends with 'f2' and not 'func'.
#
obj = getattr(mod, name)
ismod = inspect.ismodule(obj)
# ispkg = ismod and obj.__package__ == obj.__name__
if not ismod and no_all:
# only allow local objects to be collected
# - at this point modules & pkgs should have already been
# filtered for direct sub-modules and pkgs
if not hasattr(obj, "__module__"):
# this would be a locally defined variable like
# plasmapy.__citation__
pass
elif not obj.__module__.startswith(pkg_name):
# object not from package being documented
mod_objs[obj_type]["names"].remove(name)
continue
if ismod:
obj_renamed = obj.__name__.split(".")[-1] != name
elif not hasattr(obj, "__name__"):
obj_renamed = False
else:
obj_renamed = obj.__name__ != name
if ismod and obj_renamed:
qualname = f"{obj.__package__}.{name}"
elif ismod and not obj_renamed:
qualname = obj.__name__
elif obj_renamed or not hasattr(obj, "__module__"):
# can not tell if the object was renamed in modname or in
# obj.__module__, so assumed it happened in modname
qualname = f"{modname}.{name}"
elif obj.__module__.split(".")[0] != pkg_name:
# this will catch scenarios like typing alias definitions where
# __module__ == typing even when defined locally
qualname = f"{modname}.{name}"
else:
qualname = f"{obj.__module__}.{name}"
mod_objs[obj_type]["qualnames"].append(qualname)
mod_objs[obj_type]["objs"].append(obj)
# sort lists
names = sorted(mod_objs[obj_type]["names"].copy())
qualnames = []
objs = []
for name in names:
index = mod_objs[obj_type]["names"].index(name)
qualnames.append(mod_objs[obj_type]["qualnames"][index])
objs.append(mod_objs[obj_type]["objs"][index])
mod_objs[obj_type] = {"names": names, "qualnames": qualnames, "objs": objs}
return mod_objs