diff --git a/.dockerignore b/.dockerignore index 6d10671731cb..45342d931a6d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ build/* publish/* -.git/ \ No newline at end of file +.git/ diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d2c60d10f7a8..4d6909da1658 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -38,10 +38,10 @@ jobs: run: | import yaml, os def add_to_env_and_outputs(name, value): - for var in ('GITHUB_OUTPUT', 'GITHUB_ENV'): + for var in ('GITHUB_OUTPUT', 'GITHUB_ENV'): with open(os.environ[var], 'a') as fh: print(f'{name}={value}', file=fh) - + with open('pyqgis_conf.yml', 'r') as f: cfg = yaml.safe_load(f) version_list = cfg['version_list'].replace(' ', '').split(',') diff --git a/.gitignore b/.gitignore index 7c298f3aa120..e38c3b4bdb5c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,3 @@ source/i18n/*/conf.py source/static/ sphinx_rtd_theme/ .token* - diff --git a/autoautosummary.py b/autoautosummary.py index 87bd3f1a51c7..f34581df5d2e 100644 --- a/autoautosummary.py +++ b/autoautosummary.py @@ -2,15 +2,15 @@ # see https://stackoverflow.com/questions/20569011/python-sphinx-autosummary-automated-listing-of-member-functions # added toctree and nosignatures in options -from sphinx.ext.autosummary import Autosummary -from sphinx.ext.autosummary import get_documenter -from docutils.parsers.rst import directives -from sphinx.util.inspect import safe_getattr -# from sphinx.directives import directive +from enum import Enum import PyQt5 from docutils import nodes -from enum import Enum +from docutils.parsers.rst import directives +from sphinx.ext.autosummary import Autosummary, get_documenter +from sphinx.util.inspect import safe_getattr + +# from sphinx.directives import directive class AutoAutoSummary(Autosummary): @@ -22,13 +22,14 @@ class AutoAutoSummary(Autosummary): see https://stackoverflow.com/questions/20569011/python-sphinx-autosummary-automated-listing-of-member-functions """ + option_spec = { - 'methods': directives.unchanged, - 'signals': directives.unchanged, - 'enums': directives.unchanged, - 'attributes': directives.unchanged, - 'nosignatures': directives.unchanged, - 'toctree': directives.unchanged + "methods": directives.unchanged, + "signals": directives.unchanged, + "enums": directives.unchanged, + "attributes": directives.unchanged, + "nosignatures": directives.unchanged, + "toctree": directives.unchanged, } required_arguments = 1 @@ -46,20 +47,22 @@ def get_members(doc, obj, typ, include_public=None, signal=False, enum=False): try: chobj = safe_getattr(obj, name) documenter = get_documenter(doc.settings.env.app, chobj, obj) - #cl = get_class_that_defined_method(chobj) - #print(name, chobj.__qualname__, type(chobj), issubclass(chobj, Enum), documenter.objtype) + # cl = get_class_that_defined_method(chobj) + # print(name, chobj.__qualname__, type(chobj), issubclass(chobj, Enum), documenter.objtype) if documenter.objtype == typ: - if typ == 'attribute': - if signal and type(chobj) != PyQt5.QtCore.pyqtSignal: + if typ == "attribute": + if signal and isinstance(chobj, PyQt5.QtCore.pyqtSignal): continue - if not signal and type(chobj) == PyQt5.QtCore.pyqtSignal: + if not signal and isinstance(chobj, PyQt5.QtCore.pyqtSignal): continue # skip monkey patched enums # the monkeypatched enums coming out of scoped enum inherit Enum # while the standard/old ones do not - if hasattr(chobj, '__objclass__') and issubclass(chobj.__objclass__, Enum): + if hasattr(chobj, "__objclass__") and issubclass( + chobj.__objclass__, Enum + ): continue - elif typ == 'class': + elif typ == "class": if enum: if not issubclass(chobj, Enum): continue @@ -68,7 +71,7 @@ def get_members(doc, obj, typ, include_public=None, signal=False, enum=False): items.append(name) except AttributeError: continue - public = [x for x in items if x in include_public or not x.startswith('_')] + public = [x for x in items if x in include_public or not x.startswith("_")] return public, items except BaseException as e: print(str(e)) @@ -80,25 +83,29 @@ def run(self): rubric_elems = None rubric_public_elems = None try: - (module_name, class_name) = clazz.rsplit('.', 1) + (module_name, class_name) = clazz.rsplit(".", 1) m = __import__(module_name, globals(), locals(), [class_name]) c = getattr(m, class_name) - if 'methods' in self.options: - rubric_title = 'Methods' - _, rubric_elems = self.get_members(self.state.document, c, 'method', ['__init__']) - elif 'enums' in self.options: - rubric_title = 'Enums' - _, rubric_elems = self.get_members(self.state.document, c, 'class', None, False, True) - elif 'signals' in self.options: - rubric_title = 'Signals' - _, rubric_elems = self.get_members(self.state.document, c, 'attribute', None, True) - elif 'attributes' in self.options: - rubric_title = 'Attributes' - _, rubric_elems = self.get_members(self.state.document, c, 'attribute', None, False) + if "methods" in self.options: + rubric_title = "Methods" + _, rubric_elems = self.get_members(self.state.document, c, "method", ["__init__"]) + elif "enums" in self.options: + rubric_title = "Enums" + _, rubric_elems = self.get_members( + self.state.document, c, "class", None, False, True + ) + elif "signals" in self.options: + rubric_title = "Signals" + _, rubric_elems = self.get_members(self.state.document, c, "attribute", None, True) + elif "attributes" in self.options: + rubric_title = "Attributes" + _, rubric_elems = self.get_members( + self.state.document, c, "attribute", None, False + ) if rubric_elems: - rubric_public_elems = list(filter(lambda e: not e.startswith('_'), rubric_elems)) - self.content = ["~%s.%s" % (clazz, elem) for elem in rubric_public_elems] + rubric_public_elems = list(filter(lambda e: not e.startswith("_"), rubric_elems)) + self.content = [f"~{clazz}.{elem}" for elem in rubric_public_elems] except BaseException as e: print(str(e)) raise e @@ -107,6 +114,6 @@ def run(self): ret = super().run() if rubric_title: if rubric_public_elems and len(rubric_public_elems) > 0: - rub = nodes.rubric('', rubric_title) + rub = nodes.rubric("", rubric_title) ret.insert(0, rub) return ret diff --git a/conf.py.in b/conf.py.in index bea256212a4b..8d005ab92d3a 100644 --- a/conf.py.in +++ b/conf.py.in @@ -216,4 +216,3 @@ def setup(app): app.connect('autodoc-skip-member', skip_member) except BaseException as e: raise e - diff --git a/process_links.py b/process_links.py index e70bd4faf767..cd7a28fbd91d 100644 --- a/process_links.py +++ b/process_links.py @@ -5,12 +5,12 @@ # # This logic has been copied from the existing extension with some tuning for PyQGIS -import re import enum -import yaml +import re +import yaml -with open('pyqgis_conf.yml', 'r') as f: +with open("pyqgis_conf.yml") as f: cfg = yaml.safe_load(f) @@ -19,40 +19,45 @@ # https://regex101.com/r/lSB3rK/2/ py_ext_sig_re = re.compile( - r'''^ ([\w.]+::)? # explicit module name + r"""^ ([\w.]+::)? # explicit module name ([\w.]+\.)? # module and/or class name(s) (\w+) \s* # thing name (?: \((.*)\) # optional: arguments (?:\s* -> \s* ([\w.]+(?:\[.*?\])?))? # return annotation (?:\s* \[(signal)\])? # is signal )? $ # and nothing more - ''', re.VERBOSE) + """, + re.VERBOSE, +) def show_inheritance(obj): # handle inheritance printing to patch qgis._core with qgis.core # https://github.com/sphinx-doc/sphinx/blob/685e3fdb49c42b464e09ec955e1033e2a8729fff/sphinx/ext/autodoc/__init__.py#L1103-L1109 - if hasattr(obj, '__bases__') and len(obj.__bases__): - bases = [b.__module__ in ('__builtin__', 'builtins') and - ':class:`%s`' % b.__name__ or - ':class:`%s.%s`' % (b.__module__, b.__name__) - for b in obj.__bases__] - return 'Bases: %s' % ', '.join(bases) + if hasattr(obj, "__bases__") and len(obj.__bases__): + bases = [ + b.__module__ in ("__builtin__", "builtins") + and ":class:`%s`" % b.__name__ + or f":class:`{b.__module__}.{b.__name__}`" + for b in obj.__bases__ + ] + return "Bases: %s" % ", ".join(bases) return None def create_links(doc: str) -> str: # fix inheritance - doc = re.sub(r'qgis\._(core|gui|analysis|processing)\.', r'', doc) + doc = re.sub(r"qgis\._(core|gui|analysis|processing)\.", r"", doc) # class - doc = re.sub(r'\b(Qgi?s[A-Z]\w+)([, )]|\. )', r':py:class:`.\1`\2', doc) + doc = re.sub(r"\b(Qgi?s[A-Z]\w+)([, )]|\. )", r":py:class:`.\1`\2", doc) return doc + def process_docstring(app, what, name, obj, options, lines): # print('d', what, name, obj, options) bases = show_inheritance(obj) if bases: - lines.insert(0, '') + lines.insert(0, "") lines.insert(0, bases) for i in range(len(lines)): @@ -62,25 +67,25 @@ def process_docstring(app, what, name, obj, options, lines): lines[i] = create_links(lines[i]) # add return type and param type - if what != 'class' and not isinstance(obj, enum.EnumMeta) and obj.__doc__: - signature = obj.__doc__.split('\n')[0] - if signature != '': + if what != "class" and not isinstance(obj, enum.EnumMeta) and obj.__doc__: + signature = obj.__doc__.split("\n")[0] + if signature != "": match = py_ext_sig_re.match(signature) if not match: print(obj) - if name not in cfg['non-instantiable']: - raise Warning('invalid signature for {}: {}'.format(name, signature)) + if name not in cfg["non-instantiable"]: + raise Warning(f"invalid signature for {name}: {signature}") else: exmod, path, base, args, retann, signal = match.groups() if args: - args = args.split(', ') + args = args.split(", ") for arg in args: try: - argname, hint = arg.split(': ') + argname, hint = arg.split(": ") except ValueError: continue - searchfor = ':param {}:'.format(argname) + searchfor = f":param {argname}:" insert_index = None for i, line in enumerate(lines): @@ -93,28 +98,25 @@ def process_docstring(app, what, name, obj, options, lines): insert_index = len(lines) if insert_index is not None: - lines.insert( - insert_index, - ':type {}: {}'.format(argname, create_links(hint)) - ) + lines.insert(insert_index, f":type {argname}: {create_links(hint)}") if retann: insert_index = len(lines) for i, line in enumerate(lines): - if line.startswith(':rtype:'): + if line.startswith(":rtype:"): insert_index = None break - elif line.startswith(':return:') or line.startswith(':returns:'): + elif line.startswith(":return:") or line.startswith(":returns:"): insert_index = i if insert_index is not None: if insert_index == len(lines): # Ensure that :rtype: doesn't get joined with a paragraph of text, which # prevents it being interpreted. - lines.append('') + lines.append("") insert_index += 1 - lines.insert(insert_index, ':rtype: {}'.format(create_links(retann))) + lines.insert(insert_index, f":rtype: {create_links(retann)}") def process_signature(app, what, name, obj, options, signature, return_annotation): @@ -125,7 +127,7 @@ def process_signature(app, what, name, obj, options, signature, return_annotatio def skip_member(app, what, name, obj, skip, options): # skip monkey patched enums (base classes are different) - if hasattr(obj, 'is_monkey_patched') and obj.is_monkey_patched: - print(f'skipping monkey patched enum {name}') + if hasattr(obj, "is_monkey_patched") and obj.is_monkey_patched: + print(f"skipping monkey patched enum {name}") return True return skip diff --git a/scripts/make_api_rst.py b/scripts/make_api_rst.py index 98f0efc6c8c0..5cf700dad179 100755 --- a/scripts/make_api_rst.py +++ b/scripts/make_api_rst.py @@ -1,47 +1,68 @@ #!/usr/bin/env python3 -# coding=utf-8 -from string import Template +import argparse from os import makedirs from shutil import rmtree +from string import Template + import yaml -import argparse -with open('pyqgis_conf.yml', 'r') as f: +with open("pyqgis_conf.yml") as f: cfg = yaml.safe_load(f) -parser = argparse.ArgumentParser(description='Create RST files for QGIS Python API Documentation') -parser.add_argument('--version', '-v', dest='qgis_version', default="master") -parser.add_argument('--package', '-p', dest='package_limit', default=None, nargs='+', - choices=['core', 'gui', 'server', 'analysis', 'processing', '_3d'], - help='limit the build of the docs to one package (core, gui, server, analysis, processing, 3d) ') -parser.add_argument('--class', '-c', dest='single_class', default=None, nargs='+', - help='limit the build of the docs to a single class') +parser = argparse.ArgumentParser(description="Create RST files for QGIS Python API Documentation") +parser.add_argument("--version", "-v", dest="qgis_version", default="master") +parser.add_argument( + "--package", + "-p", + dest="package_limit", + default=None, + nargs="+", + choices=["core", "gui", "server", "analysis", "processing", "_3d"], + help="limit the build of the docs to one package (core, gui, server, analysis, processing, 3d) ", +) +parser.add_argument( + "--class", + "-c", + dest="single_class", + default=None, + nargs="+", + help="limit the build of the docs to a single class", +) args = parser.parse_args() if args.package_limit: packages = args.package_limit - exec("from qgis import {}".format(', '.join(packages))) + exec("from qgis import {}".format(", ".join(packages))) packages = {pkg: eval(pkg) for pkg in packages} else: - from qgis import core, gui, analysis, server, processing, _3d - packages = {'core': core, 'gui': gui, 'analysis': analysis, 'server': server, 'processing': processing, '_3d': _3d} + from qgis import _3d, analysis, core, gui, processing, server + + packages = { + "core": core, + "gui": gui, + "analysis": analysis, + "server": server, + "processing": processing, + "_3d": _3d, + } def ltr_tag(v): try: - pr = int(v.split('.')[1]) # 3.22 => 22 - if (pr+2) % 3 == 0: # LTR is every 3 releases starting at 3.4 - return ' (LTR)' + pr = int(v.split(".")[1]) # 3.22 => 22 + if (pr + 2) % 3 == 0: # LTR is every 3 releases starting at 3.4 + return " (LTR)" except IndexError: pass - return '' - + return "" -version_list = cfg['version_list'].replace(' ', '').split(',') -version_links = ', '.join([f'`{v}{ltr_tag(v)} `_' for v in version_list if v != 'master']) +version_list = cfg["version_list"].replace(" ", "").split(",") +version_links = ", ".join( + [f"`{v}{ltr_tag(v)} `_" for v in version_list if v != "master"] +) # Make sure :numbered: is only specified in the top level index - see @@ -124,18 +145,18 @@ def generate_docs(): sphinx command to generate the actual html output. """ - #qgis_version = 'master' + # qgis_version = 'master' qgis_version = args.qgis_version - rmtree('build/{}'.format(qgis_version), ignore_errors=True) - rmtree('api/{}'.format(qgis_version), ignore_errors=True) - makedirs('api', exist_ok=True) - makedirs('api/{}'.format(qgis_version)) - index = open('api/{}/index.rst'.format(qgis_version), 'w') + rmtree(f"build/{qgis_version}", ignore_errors=True) + rmtree(f"api/{qgis_version}", ignore_errors=True) + makedirs("api", exist_ok=True) + makedirs(f"api/{qgis_version}") + index = open(f"api/{qgis_version}/index.rst", "w") # Read in the standard rst template we will use for classes index.write(document_header) - with open('rst/qgis_pydoc_template.txt', 'r') as template_file: + with open("rst/qgis_pydoc_template.txt") as template_file: template_text = template_file.read() template = Template(template_text) @@ -143,28 +164,21 @@ def generate_docs(): # template based on standard rst template for package_name, package in packages.items(): - makedirs('api/{}/{}'.format(qgis_version, package_name)) - index.write(' {}/index\n'.format(package_name)) + makedirs(f"api/{qgis_version}/{package_name}") + index.write(f" {package_name}/index\n") - package_index = open('api/{}/{}/index.rst'.format(qgis_version, package_name), 'w') + package_index = open(f"api/{qgis_version}/{package_name}/index.rst", "w") # Read in the standard rst template we will use for classes - package_index.write(package_header.replace('PACKAGENAME', package_name)) + package_index.write(package_header.replace("PACKAGENAME", package_name)) for class_name in extract_package_classes(package): print(class_name) - substitutions = { - 'PACKAGE': package_name, - 'CLASS': class_name - } + substitutions = {"PACKAGE": package_name, "CLASS": class_name} class_template = template.substitute(**substitutions) - class_rst = open( - 'api/{}/{}/{}.rst'.format( - qgis_version, package_name, class_name - ), 'w' - ) + class_rst = open(f"api/{qgis_version}/{package_name}/{class_name}.rst", "w") print(class_template, file=class_rst) class_rst.close() - package_index.write(' {}\n'.format(class_name)) + package_index.write(f" {class_name}\n") package_index.close() index.write(document_footer) @@ -183,7 +197,7 @@ def extract_package_classes(package): classes = [] for class_name in dir(package): - if class_name.startswith('_'): + if class_name.startswith("_"): continue if args.single_class: found = False @@ -193,7 +207,7 @@ def extract_package_classes(package): break if not found: continue - if class_name in cfg['skipped']: + if class_name in cfg["skipped"]: continue # if not re.match('^Qgi?s', class_name): # continue