From 21ea644cc1ebaeb694472d46a0b3e62cbb7e31c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 15:07:48 -0500 Subject: [PATCH] Use up-to-date Sphinx --- docs/Makefile | 11 +- docs/conf.py | 210 +++---------------- docs/development/includes.py | 365 ---------------------------------- docs/development/includes.rst | 2 - docs/generate-tagfile | 32 --- 5 files changed, 28 insertions(+), 592 deletions(-) delete mode 100644 docs/development/includes.py delete mode 100644 docs/development/includes.rst delete mode 100755 docs/generate-tagfile diff --git a/docs/Makefile b/docs/Makefile index bb84c1dba..7efd36bac 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,7 +1,6 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build -FFMPEGDIR = _ffmpeg PYAV_PIP ?= pip PIP := $(PYAV_PIP) ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -10,14 +9,6 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . default: html -TAGFILE := _build/doxygen/tagfile.xml - -$(TAGFILE): - @if [ ! -d "$(FFMPEGDIR)" ]; then \ - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR); \ - fi - ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) - TEMPLATES := $(wildcard api/*.py development/*.py) RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) @@ -30,7 +21,7 @@ clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) - $(PIP) install -U sphinx==5.1.0 + $(PIP) install -U sphinx $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: diff --git a/docs/conf.py b/docs/conf.py index 789fc0756..1f2541f32 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,23 +1,10 @@ -import logging import os import re import sys -import xml.etree.ElementTree as etree +import sphinx from docutils import nodes -from sphinx import addnodes from sphinx.util.docutils import SphinxDirective -import sphinx - - -logging.basicConfig() - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath("..")) - -# -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -31,37 +18,24 @@ "sphinx.ext.doctest", ] - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix of source filenames. source_suffix = ".rst" - -# The master toctree document. master_doc = "index" - -# General information about the project. project = "PyAV" copyright = "2025, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + about = {} with open("../av/about.py") as fp: exec(fp.read(), about) -# The full version, including alpha/beta/rc tags. release = about["__version__"] - -# The short X.Y version. version = release.split("-")[0] - exclude_patterns = ["_build"] - -# The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output --------------------------------------------------- @@ -85,7 +59,6 @@ doctest_global_setup = """ - import errno import os @@ -109,29 +82,20 @@ def sandboxed(*args, **kwargs): os.chdir(here) video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - """ -doctest_global_cleanup = """ - -os.chdir(_cwd) - -""" - - +doctest_global_cleanup = "os.chdir(_cwd)" doctest_test_doctest_blocks = "" - extlinks = { - "ffstruct": ("http://ffmpeg.org/doxygen/trunk/struct%s.html", "struct "), - "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#"), - "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#"), - "gh-user": ("https://github.com/%s", "@"), + "ffmpeg": ("https://ffmpeg.org/doxygen/trunk/%s.html", "%s"), + "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), + "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), + "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), + "gh-user": ("https://github.com/%s", "@%s"), } -intersphinx_mapping = { - "https://docs.python.org/3": None, -} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} autodoc_member_order = "bysource" autodoc_default_options = { @@ -238,152 +202,32 @@ def makerow(*texts): ) seen = set() - if hasattr(enum, "_by_name"): # Our custom enum class - enum_items = enum._by_name.items() - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = item.__doc__ or "-" - tbody += makerow(attr, name, value, doc) - - return [table] - else: # Standard IntEnum - enum_items = [ - (name, item) - for name, item in vars(enum).items() - if isinstance(item, enum) - ] - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = enum.__annotations__.get(name, "---")[1:-1] - tbody += makerow(attr, name, value, doc) - - return [table] - - -doxylink = {} -ffmpeg_tagfile = os.path.abspath( - os.path.join(__file__, "..", "_build", "doxygen", "tagfile.xml") -) -if not os.path.exists(ffmpeg_tagfile): - print("ERROR: Missing FFmpeg tagfile.") - exit(1) -doxylink["ffmpeg"] = (ffmpeg_tagfile, "https://ffmpeg.org/doxygen/trunk/") - - -def doxylink_create_handler(app, file_name, url_base): - print("Finding all names in Doxygen tagfile", file_name) - - doc = etree.parse(file_name) - root = doc.getroot() - - parent_map = {} # ElementTree doesn't five us access to parents. - urls = {} - - for node in root.findall(".//name/.."): - for child in node: - parent_map[child] = node - - kind = node.attrib["kind"] - if kind not in ("function", "struct", "variable"): - continue - - name = node.find("name").text - - if kind not in ("function",): - parent = parent_map.get(node) - parent_name = parent.find("name") if parent else None - if parent_name is not None: - name = f"{parent_name.text}.{name}" - - filenode = node.find("filename") - if filenode is not None: - url = filenode.text - else: - url = "{}#{}".format( - node.find("anchorfile").text, - node.find("anchor").text, - ) - - urls.setdefault(kind, {})[name] = url - - def get_url(name): - # These are all the kinds that seem to exist. - for kind in ( - "function", - "struct", - "variable", # These are struct members. - # 'class', - # 'define', - # 'enumeration', - # 'enumvalue', - # 'file', - # 'group', - # 'page', - # 'typedef', - # 'union', - ): + enum_items = [ + (name, item) + for name, item in vars(enum).items() + if isinstance(item, enum) + ] + for name, item in enum_items: + if name.lower() in seen: + continue + seen.add(name.lower()) + try: - return urls[kind][name] + attr = properties[item] except KeyError: - pass - - def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): - m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) - title, name = m.groups() - name = name or title - - url = get_url(name) - if not url: - if name == "AVFrame.color_primaries": - url = "structAVFrame.html#a59a3f830494f2ed1133103a1bc9481e7" - elif name == "AVFrame.color_trc": - url = "structAVFrame.html#ab09abb126e3922bc1d010cf044087939" - else: - print("ERROR: Could not find", name) - exit(1) - - node = addnodes.literal_strong(title, title) - if url: - url = url_base + url - node = nodes.reference("", "", node, refuri=url) + if cls: + continue + attr = None - return [node], [] + value = f"0x{item.value:X}" + doc = enum.__annotations__.get(name, "---")[1:-1] + tbody += makerow(attr, name, value, doc) - return _doxylink_handler + return [table] def setup(app): app.add_css_file("custom.css") - app.add_directive("flagtable", EnumTable) app.add_directive("enumtable", EnumTable) app.add_directive("pyinclude", PyInclude) - - skip = os.environ.get("PYAV_SKIP_DOXYLINK") - for role, (filename, url_base) in doxylink.items(): - if skip: - app.add_role(role, lambda *args: ([], [])) - else: - app.add_role(role, doxylink_create_handler(app, filename, url_base)) diff --git a/docs/development/includes.py b/docs/development/includes.py deleted file mode 100644 index 8f350a81e..000000000 --- a/docs/development/includes.py +++ /dev/null @@ -1,365 +0,0 @@ -import json -import os -import re -import sys - -import xml.etree.ElementTree as etree - -from Cython.Compiler.Main import CompilationOptions, Context -from Cython.Compiler.TreeFragment import parse_from_strings -from Cython.Compiler.Visitor import TreeVisitor -from Cython.Compiler import Nodes - -os.chdir(os.path.abspath(os.path.join(__file__, '..', '..', '..'))) - - -class Visitor(TreeVisitor): - - def __init__(self, state=None): - super(Visitor, self).__init__() - self.state = dict(state or {}) - self.events = [] - - def record_event(self, node, **kw): - state = self.state.copy() - state.update(**kw) - state['node'] = node - state['pos'] = node.pos - state['end_pos'] = node.end_pos() - self.events.append(state) - - def visit_Node(self, node): - self.visitchildren(node) - - def visit_ModuleNode(self, node): - self.state['module'] = node.full_module_name - self.visitchildren(node) - self.state.pop('module') - - def visit_CDefExternNode(self, node): - self.state['extern_from'] = node.include_file - self.visitchildren(node) - self.state.pop('extern_from') - - def visit_CStructOrUnionDefNode(self, node): - self.record_event(node, type='struct', name=node.name) - self.state['struct'] = node.name - self.visitchildren(node) - self.state.pop('struct') - - def visit_CFuncDeclaratorNode(self, node): - if isinstance(node.base, Nodes.CNameDeclaratorNode): - self.record_event(node, type='function', name=node.base.name) - else: - self.visitchildren(node) - - def visit_CVarDefNode(self, node): - - if isinstance(node.declarators[0], Nodes.CNameDeclaratorNode): - - # Grab the type name. - # TODO: Do a better job. - type_ = node.base_type - if hasattr(type_, 'name'): - type_name = type_.name - elif hasattr(type_, 'base_type'): - type_name = type_.base_type.name - else: - type_name = str(type_) - - self.record_event(node, type='variable', name=node.declarators[0].name, - vartype=type_name) - - else: - self.visitchildren(node) - - def visit_CClassDefNode(self, node): - self.state['class'] = node.class_name - self.visitchildren(node) - self.state.pop('class') - - def visit_PropertyNode(self, node): - self.state['property'] = node.name - self.visitchildren(node) - self.state.pop('property') - - def visit_DefNode(self, node): - self.state['function'] = node.name - self.visitchildren(node) - self.state.pop('function') - - def visit_AttributeNode(self, node): - if getattr(node.obj, 'name', None) == 'lib': - self.record_event(node, type='use', name=node.attribute) - else: - self.visitchildren(node) - - -def extract(path, **kwargs): - - name = os.path.splitext(os.path.relpath(path))[0].replace('/', '.') - - options = CompilationOptions() - options.include_path.append('include') - options.language_level = 2 - options.compiler_directives = dict( - c_string_type='str', - c_string_encoding='ascii', - ) - - context = Context( - options.include_path, - options.compiler_directives, - options.cplus, - options.language_level, - options=options, - ) - - tree = parse_from_strings( - name, open(path).read(), context, - level='module_pxd' if path.endswith('.pxd') else None, - **kwargs) - - extractor = Visitor({'file': path}) - extractor.visit(tree) - return extractor.events - - -def iter_cython(path): - '''Yield all ``.pyx`` and ``.pxd`` files in the given root.''' - for dir_path, dir_names, file_names in os.walk(path): - for file_name in file_names: - if file_name.startswith('.'): - continue - if os.path.splitext(file_name)[1] not in ('.pyx', '.pxd'): - continue - yield os.path.join(dir_path, file_name) - - -doxygen = {} -doxygen_base = 'https://ffmpeg.org/doxygen/trunk' -tagfile_path = 'docs/_build/doxygen/tagfile.xml' - -tagfile_json = tagfile_path + '.json' -if os.path.exists(tagfile_json): - print('Loading pre-parsed Doxygen tagfile:', tagfile_json, file=sys.stderr) - doxygen = json.load(open(tagfile_json)) - - -if not doxygen: - - print('Parsing Doxygen tagfile:', tagfile_path, file=sys.stderr) - if not os.path.exists(tagfile_path): - print(' MISSING!', file=sys.stderr) - else: - - root = etree.parse(tagfile_path) - - def inspect_member(node, name_prefix=''): - name = name_prefix + node.find('name').text - anchorfile = node.find('anchorfile').text - anchor = node.find('anchor').text - - url = '%s/%s#%s' % (doxygen_base, anchorfile, anchor) - - doxygen[name] = {'url': url} - - if node.attrib['kind'] == 'function': - ret_type = node.find('type').text - arglist = node.find('arglist').text - sig = '%s %s%s' % (ret_type, name, arglist) - doxygen[name]['sig'] = sig - - for struct in root.iter('compound'): - if struct.attrib['kind'] != 'struct': - continue - name_prefix = struct.find('name').text + '.' - for node in struct.iter('member'): - inspect_member(node, name_prefix) - - for node in root.iter('member'): - inspect_member(node) - - - json.dump(doxygen, open(tagfile_json, 'w'), sort_keys=True, indent=4) - - -print('Parsing Cython source for references...', file=sys.stderr) -lib_references = {} -for path in iter_cython('av'): - try: - events = extract(path) - except Exception as e: - print(" %s in %s" % (e.__class__.__name__, path), file=sys.stderr) - print(" %s" % e, file=sys.stderr) - continue - for event in events: - if event['type'] == 'use': - lib_references.setdefault(event['name'], []).append(event) - - - - - - - -defs_by_extern = {} -for path in iter_cython('include'): - - # This one has "include" directives, which is not supported when - # parsing from a string. - if path == 'include/libav.pxd': - continue - - # Extract all #: comments from the source files. - comments_by_line = {} - for i, line in enumerate(open(path)): - m = re.match(r'^\s*#: ?', line) - if m: - comment = line[m.end():].rstrip() - comments_by_line[i + 1] = line[m.end():] - - # Extract Cython definitions from the source files. - for event in extract(path): - - extern = event.get('extern_from') or path.replace('include/', '') - defs_by_extern.setdefault(extern, []).append(event) - - # Collect comments above and below - comments = event['_comments'] = [] - line = event['pos'][1] - 1 - while line in comments_by_line: - comments.insert(0, comments_by_line.pop(line)) - line -= 1 - line = event['end_pos'][1] + 1 - while line in comments_by_line: - comments.append(comments_by_line.pop(line)) - line += 1 - - # Figure out the Sphinx headline. - if event['type'] == 'function': - event['_sort_key'] = 2 - sig = doxygen.get(event['name'], {}).get('sig') - if sig: - sig = re.sub(r'\).+', ')', sig) # strip trailer - event['_headline'] = '.. c:function:: %s' % sig - else: - event['_headline'] = '.. c:function:: %s()' % event['name'] - - elif event['type'] == 'variable': - struct = event.get('struct') - if struct: - event['_headline'] = '.. c:member:: %s %s' % (event['vartype'], event['name']) - event['_sort_key'] = 1.1 - else: - event['_headline'] = '.. c:var:: %s' % event['name'] - event['_sort_key'] = 3 - - elif event['type'] == 'struct': - event['_headline'] = '.. c:type:: struct %s' % event['name'] - event['_sort_key'] = 1 - event['_doxygen_url'] = '%s/struct%s.html' % (doxygen_base, event['name']) - - else: - print('Unknown event type %s' % event['type'], file=sys.stderr) - - name = event['name'] - if event.get('struct'): - name = '%s.%s' % (event['struct'], name) - - # Doxygen URLs - event.setdefault('_doxygen_url', doxygen.get(name, {}).get('url')) - - # Find use references. - ref_events = lib_references.get(name, []) - if ref_events: - - ref_pairs = [] - for ref in sorted(ref_events, key=lambda e: e['name']): - - chunks = [ - ref.get('module'), - ref.get('class'), - ] - chunks = filter(None, chunks) - prefix = '.'.join(chunks) + '.' if chunks else '' - - if ref.get('property'): - ref_pairs.append((ref['property'], ':attr:`%s%s`' % (prefix, ref['property']))) - elif ref.get('function'): - name = ref['function'] - if name in ('__init__', '__cinit__', '__dealloc__'): - ref_pairs.append((name, ':class:`%s%s <%s>`' % (prefix, name, prefix.rstrip('.')))) - else: - ref_pairs.append((name, ':func:`%s%s`' % (prefix, name))) - else: - continue - - unique_refs = event['_references'] = [] - seen = set() - for name, ref in sorted(ref_pairs): - if name in seen: - continue - seen.add(name) - unique_refs.append(ref) - - - - -print(''' - -.. - This file is generated by includes.py; any modifications will be destroyed! - -Wrapped C Types and Functions -============================= - -''') - -for extern, events in sorted(defs_by_extern.items()): - did_header = False - - for event in events: - - headline = event.get('_headline') - comments = event.get('_comments') - refs = event.get('_references', []) - url = event.get('_doxygen_url') - indent = ' ' if event.get('struct') else '' - - if not headline: - continue - if ( - not filter(None, (x.strip() for x in comments if x.strip())) and - not refs and - event['type'] not in ('struct', ) - ): - pass - - if not did_header: - print('``%s``' % extern) - print('-' * (len(extern) + 4)) - print() - did_header = True - - if url: - print() - print(indent + '.. rst-class:: ffmpeg-quicklink') - print() - print(indent + ' `FFmpeg Docs <%s>`__' % url) - - print(indent + headline) - print() - - if comments: - for line in comments: - print(indent + ' ' + line) - print() - - if refs: - print(indent + ' Referenced by: ', end='') - for i, ref in enumerate(refs): - print((', ' if i else '') + ref, end='') - print('.') - - print() diff --git a/docs/development/includes.rst b/docs/development/includes.rst deleted file mode 100644 index 6b2c989cb..000000000 --- a/docs/development/includes.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. include:: ../_build/rst/development/includes.rst diff --git a/docs/generate-tagfile b/docs/generate-tagfile deleted file mode 100755 index 1f729de5c..000000000 --- a/docs/generate-tagfile +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os -import subprocess - - -parser = argparse.ArgumentParser() -parser.add_argument("-l", "--library", required=True) -parser.add_argument("-o", "--output", required=True) -args = parser.parse_args() - -output = os.path.abspath(args.output) -outdir = os.path.dirname(output) -if not os.path.exists(outdir): - os.makedirs(outdir) - -proc = subprocess.Popen(["doxygen", "-"], stdin=subprocess.PIPE, cwd=args.library) -proc.communicate( - """ - -#@INCLUDE = doc/Doxyfile -GENERATE_TAGFILE = {} -GENERATE_HTML = no -GENERATE_LATEX = no -CASE_SENSE_NAMES = yes -INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale - -""".format( - output - ).encode() -)