Source code for schedula.ext.autosummary

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2015-2025, Vincenzo Arcidiacono;
# Licensed under the EUPL (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl

"""
It is a patch to sphinx.ext.autosummary.
"""
import warnings
import logging
from pathlib import Path
from typing import Any
from sphinx.util.inspect import getall
from sphinx.util.osutil import ensuredir
from sphinx.ext.autosummary import (
    import_by_name, _get_documenter, get_rst_suffix, mock, import_ivar_by_name,
    ImportExceptionGroup
)
from sphinx.ext.autosummary.generate import (
    find_autosummary_in_files, AutosummaryRenderer, ModuleScanner, _get_members,
    _get_module_attrs, _get_modules, _split_full_qualified_name
)
from sphinx.locale import __

logger = logging.getLogger(__name__)
warnings.filterwarnings(
    'ignore', category=DeprecationWarning, module='docutils'
)


[docs] def generate_autosummary_content( name, obj, parent, template, template_name, imported_members, recursive, context, modname, qualname=None, *, config, events, registry, ): doc = _get_documenter(obj, parent, registry=registry) ns = {} ns.update(context) if doc.objtype == 'module': scanner = ModuleScanner(obj, config=config, events=events, registry=registry) ns['members'] = scanner.scan(imported_members) respect_module_all = not config.autosummary_ignore_module_all imported_members = imported_members or ( '__all__' in dir(obj) and respect_module_all ) ns['functions'], ns['all_functions'] = _get_members( doc, obj, {'function'}, config=config, events=events, registry=registry, imported=imported_members, ) ns['classes'], ns['all_classes'] = _get_members( doc, obj, {'class'}, config=config, events=events, registry=registry, imported=imported_members, ) ns['exceptions'], ns['all_exceptions'] = _get_members( doc, obj, {'exception'}, config=config, events=events, registry=registry, imported=imported_members, ) ns['attributes'], ns['all_attributes'] = _get_module_attrs(name, ns[ 'members']) ns['dispatchers'], ns['all_dispatchers'] = _get_members( doc, obj, {'dispatcher'}, config=config, events=events, registry=registry, imported=imported_members, ) ispackage = hasattr(obj, '__path__') if ispackage and recursive: # Use members that are not modules as skip list, because it would then mean # that module was overwritten in the package namespace skip = ( ns['all_functions'] + ns['all_classes'] + ns['all_exceptions'] + ns['all_attributes'] ) # If respect_module_all and module has a __all__ attribute, first get # modules that were explicitly imported. Next, find the rest with the # get_modules method, but only put in "public" modules that are in the # __all__ list # # Otherwise, use get_modules method normally if respect_module_all and '__all__' in dir(obj): imported_modules, all_imported_modules = _get_members( doc, obj, {'module'}, config=config, events=events, registry=registry, imported=True, ) skip += all_imported_modules public_members = getall(obj) else: imported_modules, all_imported_modules = [], [] public_members = None modules, all_modules = _get_modules( obj, skip=skip, name=name, public_members=public_members ) ns['modules'] = imported_modules + modules ns['all_modules'] = all_imported_modules + all_modules elif doc.objtype == 'class': ns['members'] = dir(obj) ns['inherited_members'] = set(dir(obj)) - set(obj.__dict__.keys()) ns['methods'], ns['all_methods'] = _get_members( doc, obj, {'method'}, config=config, events=events, registry=registry, include_public={'__init__'}, ) ns['attributes'], ns['all_attributes'] = _get_members( doc, obj, {'attribute', 'property'}, config=config, events=events, registry=registry, ) if modname is None or qualname is None: modname, qualname = _split_full_qualified_name(name) if doc.objtype in {'method', 'attribute', 'property'}: ns['class'] = qualname.rsplit('.', 1)[0] if doc.objtype == 'class': shortname = qualname else: shortname = qualname.rsplit('.', 1)[-1] ns['fullname'] = name ns['module'] = modname ns['objname'] = qualname ns['name'] = shortname ns['objtype'] = doc.objtype ns['underline'] = len(name) * '=' if template_name: return template.render(template_name, ns) else: return template.render(doc.objtype, ns)
[docs] def generate_autosummary_docs( sources, output_dir=None, suffix='.rst', base_path=None, imported_members=False, app=None, overwrite=True, encoding='utf-8', ): """Generate autosummary documentation for the given sources. :returns: list of generated files (both new and existing ones) """ assert app is not None, 'app is required' showed_sources = sorted(sources) if len(showed_sources) > 20: showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:] logger.info( __('[autosummary] generating autosummary for: %s'), ', '.join(showed_sources) ) if output_dir: logger.info(__('[autosummary] writing to %s'), output_dir) if base_path is not None: base_path = Path(base_path) source_paths = [base_path / filename for filename in sources] else: source_paths = list(map(Path, sources)) template = AutosummaryRenderer(app) # read items = find_autosummary_in_files(source_paths) # keep track of new files new_files = [] all_files = [] filename_map = app.config.autosummary_filename_map # write for entry in sorted(set(items), key=str): if entry.path is None: # The corresponding autosummary:: directive did not have # a :toctree: option continue path = output_dir or Path(entry.path).resolve() ensuredir(path) try: name, obj, parent, modname = import_by_name(entry.name) qualname = name.replace(modname + '.', '') except ImportExceptionGroup as exc: try: # try to import as an instance attribute name, obj, parent, modname = import_ivar_by_name(entry.name) qualname = name.replace(modname + '.', '') except ImportError as exc2: if exc2.__cause__: exceptions: list[BaseException] = [*exc.exceptions, exc2.__cause__] else: exceptions = [*exc.exceptions, exc2] errors = list( {f'* {type(e).__name__}: {e}' for e in exceptions} ) logger.warning( __('[autosummary] failed to import %s.\nPossible hints:\n%s'), entry.name, '\n'.join(errors), ) continue context: dict[str, Any] = {**app.config.autosummary_context} content = generate_autosummary_content( name, obj, parent, template, entry.template, imported_members, entry.recursive, context, modname, qualname, config=app.config, events=app.events, registry=app.registry, ) file_path = Path(path, filename_map.get(name, name) + suffix) all_files.append(file_path) if file_path.is_file(): with file_path.open(encoding=encoding) as f: old_content = f.read() if content == old_content: continue if overwrite: # content has changed with file_path.open('w', encoding=encoding) as f: f.write(content) new_files.append(file_path) else: with open(file_path, 'w', encoding=encoding) as f: f.write(content) new_files.append(file_path) # descend recursively to new files if new_files: all_files.extend( generate_autosummary_docs( [str(f) for f in new_files], output_dir=output_dir, suffix=suffix, base_path=base_path, imported_members=imported_members, app=app, overwrite=overwrite, ) ) return all_files
[docs] def process_generate_options(app): genfiles = app.config.autosummary_generate if genfiles is True: env = app.env genfiles = [ str(env.doc2path(x, base=False)) for x in env.found_docs if env.doc2path(x).is_file() ] elif genfiles is False: pass else: ext = list(app.config.source_suffix) genfiles = [ genfile + (ext[0] if not genfile.endswith(tuple(ext)) else '') for genfile in genfiles ] for entry in genfiles[:]: if not (app.srcdir / entry).is_file(): logger.warning(__('autosummary_generate: file not found: %s'), entry) genfiles.remove(entry) if not genfiles: return suffix = get_rst_suffix(app) if suffix is None: logger.warning( __( 'autosummary generates .rst files internally. ' 'But your source_suffix does not contain .rst. Skipped.' ) ) return imported_members = app.config.autosummary_imported_members with mock(app.config.autosummary_mock_imports): generate_autosummary_docs( genfiles, suffix=suffix, base_path=app.srcdir, app=app, imported_members=imported_members, overwrite=app.config.autosummary_generate_overwrite, encoding=app.config.source_encoding, )
[docs] def setup(app): app.setup_extension('sphinx.ext.autosummary') app.add_config_value('autosummary_skip_members', [], 'html') # replace callback process_generate_options of 'builder-inited' event. import sphinx.ext.autosummary as mdl pgo = mdl.process_generate_options event = 'builder-inited' listeners = app.events.listeners[event] from sphinx.events import EventListener for i, event in enumerate(listeners): if pgo in event: listeners[i] = EventListener(*( process_generate_options if e is pgo else e for e in event ))