# pylint: disable=dangerous-default-value # pylint: disable=global-statement # pylint: disable=too-many-locals """Module implementing Sphinx custom roles for Tezos documentation.""" import os import os.path import re from pathlib import Path from typing import Tuple from docutils import nodes from gitlab_custom_role import gitlab_role TEZOS_HOME = '../' OPAM_CACHE = None def setup(app): """Perform initializations.""" app.add_role('package', package_role) app.add_role('package-name', package_role) app.add_role('package-api', package_api_role) app.add_role('opam', opam_role) app.add_role('src', src_role) app.add_role('gl', gitlab_role) global OPAM_CACHE if OPAM_CACHE is not None: raise ValueError('package_role: opam cache already set!!!') OPAM_CACHE = {} for path, _, files in os.walk('..'): if re.match("^../_opam/", path): continue for file in files: parts = re.match("^([^/]*)[.]opam$", file) if parts: name = parts.group(1) if name in OPAM_CACHE: print( "package_role: ignoring package ", name, " at ", path, ", already found at ", OPAM_CACHE[name], ) OPAM_CACHE[name] = path.lstrip('../') print("package_role: cached", len(OPAM_CACHE), "opam packages") return {'parallel_read_safe': True} def find_dot_opam(name): """Look up an opam module.""" if name in OPAM_CACHE: return OPAM_CACHE[name] raise ValueError('opam file ' + name + '.opam does not exist in the odoc') def parse_role(text: str) -> Tuple[str, str]: """Parse the role text and target.""" parts = re.match(r"^([^<>]*)<([^<>]*)>$", text) if parts: description = parts.group(1) obj = parts.group(2) else: description = text obj = text return (description, obj) def package_role(name, rawtext, text, lineno, inliner, options={}, _content=[]): """Implement the package(-name)? roles.""" rel_lvl = inliner.document.current_source.replace(os.getcwd(), '').count( '/' ) url_prefix = '../' * (rel_lvl - 1) (text, path) = parse_role(text) # no '/' allowed in the package name: (pkg, _sep, _rest) = path.partition("/") if pkg != path: msg = inliner.reporter.error( 'package_role: garbage found in package name ' + path, line=lineno ) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] try: find_dot_opam(pkg) # check that the package exists except ValueError as err: msg = inliner.reporter.error(format(err), line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] if name == 'package-name': node = nodes.literal(text, text) return [node], [] # name == 'package' if os.path.isdir('_build/api/odoc/_html/' + pkg): if os.path.isdir( os.path.join( '_build', 'api', 'odoc', '_html', pkg, pkg.replace('-', '_').capitalize(), ) ): path = pkg + '/' + pkg.replace('-', '_').capitalize() elif os.path.isdir('_build/api/odoc/_html/'): # odoc was run but did not generate the page for an axisting package # (this is really weird, kind of internal error) raise ValueError('package_role: no API for package ', pkg) url = url_prefix + "api/odoc/_html/" + path + '/index.html' node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] def package_api_role( _name, rawtext, text, lineno, inliner, options={}, _content=[] ): """Implement the package-api role.""" rel_lvl = inliner.document.current_source.replace(os.getcwd(), '').count( '/' ) url_prefix = '../' * (rel_lvl - 1) (text, path) = parse_role(text) (pkg, _sep, _rest) = path.partition("/") try: # check that the package exists find_dot_opam(pkg) except ValueError as err: msg = inliner.reporter.error(format(err), line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] (file, _sep, _section) = path.partition('#') # remove eventual section if os.path.isdir('_build/api/odoc/_html/') and not os.path.isfile( '_build/api/odoc/_html/' + file ): # odoc was run but did not generate the page => path is wrong # (this is probably a user error) err = 'package_role: no API ' + path msg = inliner.reporter.error(format(err), line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] url = url_prefix + "api/odoc/_html/" + path node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] def opam_role(_name, rawtext, text, _lineno, _inliner, options={}, _content=[]): """Implement the opam role.""" (text, lib) = parse_role(text) tagged = re.match('([^.]+)[.].*', lib) if tagged: url = "https://opam.ocaml.org/packages/" + tagged.group(1) + "/" + lib else: url = "https://opam.ocaml.org/packages/" + lib node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] def src_role(_name, rawtext, text, lineno, inliner, options={}, _content=[]): """Implement the src role.""" (text, src) = parse_role(text) # raise a warning if the file does not exist file = src if not Path(TEZOS_HOME, file).exists(): msg = [ inliner.reporter.warning( f'source file {file} does not exist in the repo', line=lineno ) ] else: msg = [] branch = os.environ.get('CI_COMMIT_REF_NAME', 'master') project_url = os.environ.get( 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL', os.environ.get('CI_PROJECT_URL', 'https://gitlab.com/tezos/tezos'), ) if Path(TEZOS_HOME, file).is_file(): url = project_url + "/-/blob/" + branch + "/" + src else: # most likely a directory url = project_url + "/-/tree/" + branch + "/" + src node = nodes.reference(rawtext, text, refuri=url, **options) return [node], msg