diff --git a/CHANGES b/CHANGES index 0d72f3e6291..9d2cdef9e04 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Deprecated * ``sphinx.environment.temp_data['gloss_entries']`` * ``sphinx.environment.BuildEnvironment.indexentries`` * ``sphinx.environment.collectors.indexentries.IndexEntriesCollector`` +* ``sphinx.ext.apidoc.INITPY`` +* ``sphinx.ext.apidoc.shall_skip()`` * ``sphinx.io.FiletypeNotFoundError`` * ``sphinx.io.get_filetype()`` * ``sphinx.pycode.ModuleAnalyzer.encoding`` @@ -76,6 +78,7 @@ Bugs fixed * #6559: Wrong node-ids are generated in glossary directive * #6986: apidoc: misdetects module name for .so file inside module * #6899: apidoc: private members are not shown even if ``--private`` given +* #6327: apidoc: Support a python package consisted of __init__.so file * #6999: napoleon: fails to parse tilde in :exc: role * #7019: gettext: Absolute path used in message catalogs * #7023: autodoc: nested partial functions are not listed diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 8a084be4de1..d009454eafa 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -56,6 +56,16 @@ The following is a list of deprecated interfaces. - 4.0 - ``sphinx.errors.FiletypeNotFoundError`` + * - ``sphinx.ext.apidoc.INITPY`` + - 2.4 + - 4.0 + - N/A + + * - ``sphinx.ext.apidoc.shall_skip()`` + - 2.4 + - 4.0 + - ``sphinx.ext.apidoc.is_skipped_package`` + * - ``sphinx.io.get_filetype()`` - 2.4 - 4.0 diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index 99cf6701672..a9196d3a67e 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -29,7 +29,7 @@ import sphinx.locale from sphinx import __display_version__, package_dir from sphinx.cmd.quickstart import EXTENSIONS -from sphinx.deprecation import RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.locale import __ from sphinx.util import rst from sphinx.util.osutil import FileAvoidWrite, ensuredir @@ -46,7 +46,6 @@ 'show-inheritance', ] -INITPY = '__init__.py' PY_SUFFIXES = ('.py', '.pyx') + tuple(EXTENSION_SUFFIXES) template_dir = path.join(package_dir, 'templates', 'apidoc') @@ -66,11 +65,31 @@ def makename(package: str, module: str) -> str: return name +def is_initpy(filename: str) -> bool: + """Check *filename* is __init__ file or not.""" + basename = path.basename(filename) + for suffix in sorted(PY_SUFFIXES, key=len, reverse=True): + if basename == '__init__' + suffix: + return True + else: + return False + + def module_join(*modnames: str) -> str: """Join module names with dots.""" return '.'.join(filter(None, modnames)) +def is_packagedir(dirname: str = None, files: List[str] = None) -> bool: + """Check given *files* contains __init__ file.""" + if files is None and dirname is None: + return False + + if files is None: + files = os.listdir(dirname) + return any(f for f in files if is_initpy(f)) + + def write_file(name: str, text: str, opts: Any) -> None: """Write the output file for module/package .""" quiet = getattr(opts, 'quiet', None) @@ -132,15 +151,14 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files: opts: Any, subs: List[str], is_namespace: bool, excludes: List[str] = [], user_template_dir: str = None) -> None: """Build the text of the file and write the file.""" - # build a list of sub packages (directories containing an INITPY file) - subpackages = [sub for sub in subs if not - shall_skip(path.join(root, sub, INITPY), opts, excludes)] + # build a list of sub packages (directories containing an __init__ file) subpackages = [module_join(master_package, subroot, pkgname) - for pkgname in subpackages] + for pkgname in subs + if not is_skipped_package(path.join(root, pkgname), opts, excludes)] # build a list of sub modules submodules = [sub.split('.')[0] for sub in py_files if not is_skipped_module(path.join(root, sub), opts, excludes) and - sub != INITPY] + not is_initpy(sub)] submodules = [module_join(master_package, subroot, modname) for modname in submodules] options = copy(OPTIONS) @@ -189,12 +207,14 @@ def create_modules_toc_file(modules: List[str], opts: Any, name: str = 'modules' def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool: """Check if we want to skip this module.""" + warnings.warn('shall_skip() is deprecated.', + RemovedInSphinx40Warning) # skip if the file doesn't exist and not using implicit namespaces if not opts.implicit_namespaces and not path.exists(module): return True # Are we a package (here defined as __init__.py, not the folder in itself) - if os.path.basename(module) == INITPY: + if is_initpy(module): # Yes, check if we have any non-excluded modules at all here all_skipped = True basemodule = path.dirname(module) @@ -207,12 +227,30 @@ def shall_skip(module: str, opts: Any, excludes: List[str] = []) -> bool: # skip if it has a "private" name and this is selected filename = path.basename(module) - if filename != '__init__.py' and filename.startswith('_') and \ - not opts.includeprivate: + if is_initpy(filename) and filename.startswith('_') and not opts.includeprivate: return True return False +def is_skipped_package(dirname: str, opts: Any, excludes: List[str] = []) -> bool: + """Check if we want to skip this module.""" + if not path.isdir(dirname): + return False + + files = glob.glob(path.join(dirname, '*.py')) + regular_package = any(f for f in files if is_initpy(f)) + if not regular_package and not opts.implicit_namespaces: + # *dirname* is not both a regular package and an implicit namespace pacage + return True + + # Check there is some showable module inside package + if all(is_excluded(path.join(dirname, f), excludes) for f in files): + # all submodules are excluded + return True + else: + return False + + def is_skipped_module(filename: str, opts: Any, excludes: List[str]) -> bool: """Check if we want to skip this module.""" if not path.exists(filename): @@ -236,7 +274,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any, implicit_namespaces = getattr(opts, 'implicit_namespaces', False) # check if the base directory is a package and get its name - if INITPY in os.listdir(rootpath) or implicit_namespaces: + if is_packagedir(rootpath) or implicit_namespaces: root_package = rootpath.split(path.sep)[-1] else: # otherwise, the base is a directory with packages @@ -248,11 +286,13 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any, py_files = sorted(f for f in files if f.endswith(PY_SUFFIXES) and not is_excluded(path.join(root, f), excludes)) - is_pkg = INITPY in py_files - is_namespace = INITPY not in py_files and implicit_namespaces + is_pkg = is_packagedir(None, py_files) + is_namespace = not is_pkg and implicit_namespaces if is_pkg: - py_files.remove(INITPY) - py_files.insert(0, INITPY) + for f in py_files[:]: + if is_initpy(f): + py_files.remove(f) + py_files.insert(0, f) elif root != rootpath: # only accept non-package at toplevel unless using implicit namespaces if not implicit_namespaces: @@ -269,7 +309,7 @@ def recurse_tree(rootpath: str, excludes: List[str], opts: Any, if is_pkg or is_namespace: # we are in a package with something to document - if subs or len(py_files) > 1 or not shall_skip(path.join(root, INITPY), opts): + if subs or len(py_files) > 1 or not is_skipped_package(root, opts): subpackage = root[len(rootpath):].lstrip(path.sep).\ replace(path.sep, '.') # if this is not a namespace or @@ -475,6 +515,13 @@ def main(argv: List[str] = sys.argv[1:]) -> int: return 0 +deprecated_alias('sphinx.ext.apidoc', + { + 'INITPY': '__init__.py', + }, + RemovedInSphinx40Warning) + + # So program can be started with "python -m sphinx.apidoc ..." if __name__ == "__main__": main()