mirror of
https://github.com/mesonbuild/meson.git
synced 2026-11-06 05:16:42 +00:00
This replaces all of the Apache blurbs at the start of each file with an `# SPDX-License-Identifier: Apache-2.0` string. It also fixes existing uses to be consistent in capitalization, and to be placed above any copyright notices. This removes nearly 3000 lines of boilerplate from the project (only python files), which no developer cares to look at. SPDX is in common use, particularly in the Linux kernel, and is the recommended format for Meson's own `project(license: )` field
391 lines
17 KiB
Python
391 lines
17 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright 2021 The Meson development team
|
|
|
|
from .generatorbase import GeneratorBase
|
|
import re
|
|
import json
|
|
|
|
from .model import (
|
|
ReferenceManual,
|
|
Function,
|
|
Method,
|
|
Object,
|
|
ObjectType,
|
|
Type,
|
|
DataTypeInfo,
|
|
ArgBase,
|
|
PosArg,
|
|
VarArgs,
|
|
Kwarg,
|
|
)
|
|
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
import typing as T
|
|
|
|
from mesonbuild import mlog
|
|
|
|
PlaceholderTypes = T.Union[None, str, bool]
|
|
FunctionDictType = T.Dict[
|
|
str,
|
|
T.Union[
|
|
PlaceholderTypes,
|
|
T.Dict[str, PlaceholderTypes],
|
|
T.Dict[str, T.Dict[str, PlaceholderTypes]],
|
|
T.Dict[str, T.List[T.Dict[str, PlaceholderTypes]]],
|
|
T.List[T.Dict[str, PlaceholderTypes]],
|
|
T.List[str],
|
|
]
|
|
]
|
|
|
|
_ROOT_BASENAME = 'Reference-manual'
|
|
|
|
_OBJ_ID_MAP = {
|
|
ObjectType.ELEMENTARY: 'elementary',
|
|
ObjectType.BUILTIN: 'builtin',
|
|
ObjectType.MODULE: 'module',
|
|
ObjectType.RETURNED: 'returned',
|
|
}
|
|
|
|
# Indent all but the first line with 4*depth spaces.
|
|
# This function is designed to be used with `dedent`
|
|
# and fstrings where multiline strings are used during
|
|
# the string interpolation.
|
|
def smart_indent(raw: str, depth: int = 3) -> str:
|
|
lines = raw.split('\n')
|
|
first_line = lines[0]
|
|
lines = [' ' * (4 * depth) + x for x in lines]
|
|
lines[0] = first_line # Do not indent the first line
|
|
return '\n'.join(lines)
|
|
|
|
def code_block(code: str) -> str:
|
|
code = dedent(code)
|
|
return f'<pre><code class="language-meson">{code}</code></pre>'
|
|
|
|
class GeneratorMD(GeneratorBase):
|
|
def __init__(self, manual: ReferenceManual, sitemap_out: Path, sitemap_in: Path, link_def_out: Path, enable_modules: bool) -> None:
|
|
super().__init__(manual)
|
|
self.sitemap_out = sitemap_out.resolve()
|
|
self.sitemap_in = sitemap_in.resolve()
|
|
self.link_def_out = link_def_out.resolve()
|
|
self.out_dir = self.sitemap_out.parent
|
|
self.enable_modules = enable_modules
|
|
self.generated_files: T.Dict[str, str] = {}
|
|
|
|
# Utility functions
|
|
def _gen_filename(self, file_id: str, *, extension: str = 'md') -> str:
|
|
parts = file_id.split('.')
|
|
assert parts[0] == 'root'
|
|
assert all([x for x in parts])
|
|
parts[0] = _ROOT_BASENAME
|
|
parts = [re.sub(r'[0-9]+_', '', x) for x in parts]
|
|
return f'{"_".join(parts)}.{extension}'
|
|
|
|
def _gen_object_file_id(self, obj: Object) -> str:
|
|
'''
|
|
Deterministically generate a unique file ID for the Object.
|
|
|
|
This ID determines where the object will be inserted in the sitemap.
|
|
'''
|
|
if obj.obj_type == ObjectType.RETURNED and obj.defined_by_module is not None:
|
|
base = self._gen_object_file_id(obj.defined_by_module)
|
|
return f'{base}.{obj.name}'
|
|
return f'root.{_OBJ_ID_MAP[obj.obj_type]}.{obj.name}'
|
|
|
|
def _link_to_object(self, obj: T.Union[Function, Object], in_code_block: bool = False) -> str:
|
|
'''
|
|
Generate a palaceholder tag for the function/method/object documentation.
|
|
This tag is then replaced in the custom hotdoc plugin.
|
|
'''
|
|
prefix = '#' if in_code_block else ''
|
|
if isinstance(obj, Object):
|
|
return f'[[{prefix}@{obj.name}]]'
|
|
elif isinstance(obj, Method):
|
|
return f'[[{prefix}{obj.obj.name}.{obj.name}]]'
|
|
elif isinstance(obj, Function):
|
|
return f'[[{prefix}{obj.name}]]'
|
|
else:
|
|
raise RuntimeError(f'Invalid argument {obj}')
|
|
|
|
def _write_file(self, data: str, file_id: str) -> None:#
|
|
''' Write the data to disk and store the id for the generated data '''
|
|
|
|
self.generated_files[file_id] = self._gen_filename(file_id)
|
|
out_file = self.out_dir / self.generated_files[file_id]
|
|
out_file.write_text(data, encoding='ascii')
|
|
mlog.log('Generated', mlog.bold(out_file.name))
|
|
|
|
def _write_template(self, data: T.Dict[str, T.Any], file_id: str, template_name: T.Optional[str] = None) -> None:
|
|
''' Render the template mustache files and write the result '''
|
|
template_dir = Path(__file__).resolve().parent / 'templates'
|
|
template_name = template_name or file_id
|
|
template_name = f'{template_name}.mustache'
|
|
template_file = template_dir / template_name
|
|
|
|
# Import here, so that other generators don't also depend on it
|
|
import chevron
|
|
result = chevron.render(
|
|
template=template_file.read_text(encoding='utf-8'),
|
|
data=data,
|
|
partials_path=template_dir.as_posix(),
|
|
warn=True,
|
|
)
|
|
|
|
self._write_file(result, file_id)
|
|
|
|
|
|
# Actual generator functions
|
|
def _gen_func_or_method(self, func: Function) -> FunctionDictType:
|
|
def render_type(typ: Type, in_code_block: bool = False) -> str:
|
|
def data_type_to_str(dt: DataTypeInfo) -> str:
|
|
base = self._link_to_object(dt.data_type, in_code_block)
|
|
if dt.holds:
|
|
return f'{base}[{render_type(dt.holds, in_code_block)}]'
|
|
return base
|
|
assert typ.resolved
|
|
return ' | '.join([data_type_to_str(x) for x in typ.resolved])
|
|
|
|
def len_stripped(s: str) -> int:
|
|
s = s.replace(']]', '')
|
|
# I know, this regex is ugly but it works.
|
|
return len(re.sub(r'\[\[(#|@)*([^\[])', r'\2', s))
|
|
|
|
def arg_anchor(arg: ArgBase) -> str:
|
|
return f'{func.name}_{arg.name.replace("<", "_").replace(">", "_")}'
|
|
|
|
def render_signature() -> str:
|
|
# Skip a lot of computations if the function does not take any arguments
|
|
if not any([func.posargs, func.optargs, func.kwargs, func.varargs]):
|
|
return f'{render_type(func.returns, True)} {func.name}()'
|
|
|
|
signature = dedent(f'''\
|
|
# {self.brief(func)}
|
|
{render_type(func.returns, True)} {func.name}(
|
|
''')
|
|
|
|
# Calculate maximum lengths of the type and name
|
|
all_args: T.List[ArgBase] = []
|
|
all_args += func.posargs
|
|
all_args += func.optargs
|
|
all_args += [func.varargs] if func.varargs else []
|
|
|
|
max_type_len = 0
|
|
max_name_len = 0
|
|
if all_args:
|
|
max_type_len = max([len_stripped(render_type(x.type)) for x in all_args])
|
|
max_name_len = max([len(x.name) for x in all_args])
|
|
|
|
# Generate some common strings
|
|
def prepare(arg: ArgBase, link: bool = True) -> T.Tuple[str, str, str, str]:
|
|
type_str = render_type(arg.type, True)
|
|
type_len = len_stripped(type_str)
|
|
type_space = ' ' * (max_type_len - type_len)
|
|
name_space = ' ' * (max_name_len - len(arg.name))
|
|
name_str = f'<b>{arg.name.replace("<", "<").replace(">", ">")}</b>'
|
|
if link:
|
|
name_str = f'<a href="#{arg_anchor(arg)}">{name_str}</a>'
|
|
|
|
return type_str, type_space, name_str, name_space
|
|
|
|
for i in func.posargs:
|
|
type_str, type_space, name_str, name_space = prepare(i)
|
|
signature += f' {type_str}{type_space} {name_str},{name_space} # {self.brief(i)}\n'
|
|
|
|
for i in func.optargs:
|
|
type_str, type_space, name_str, name_space = prepare(i)
|
|
signature += f' {type_str}{type_space} [{name_str}],{name_space} # {self.brief(i)}\n'
|
|
|
|
if func.varargs:
|
|
type_str, type_space, name_str, name_space = prepare(func.varargs, link=False)
|
|
signature += f' {type_str}{type_space} {name_str}...,{name_space} # {self.brief(func.varargs)}\n'
|
|
|
|
# Abort if there are no kwargs
|
|
if not func.kwargs:
|
|
return signature + ')'
|
|
|
|
# Only add this separator if there are any posargs
|
|
if all_args:
|
|
signature += '\n # Keyword arguments:\n'
|
|
|
|
# Recalculate lengths for kwargs
|
|
all_args = list(func.kwargs.values())
|
|
max_type_len = max([len_stripped(render_type(x.type)) for x in all_args])
|
|
max_name_len = max([len(x.name) for x in all_args])
|
|
|
|
for kwarg in self.sorted_and_filtered(list(func.kwargs.values())):
|
|
type_str, type_space, name_str, name_space = prepare(kwarg)
|
|
required = ' <i>[required]</i> ' if kwarg.required else ' '
|
|
required = required if any([x.required for x in func.kwargs.values()]) else ''
|
|
signature += f' {name_str}{name_space} : {type_str}{type_space} {required} # {self.brief(kwarg)}\n'
|
|
|
|
return signature + ')'
|
|
|
|
def gen_arg_data(arg: T.Union[PosArg, Kwarg, VarArgs], *, optional: bool = False) -> T.Dict[str, PlaceholderTypes]:
|
|
data: T.Dict[str, PlaceholderTypes] = {
|
|
'row-id': arg_anchor(arg),
|
|
'name': arg.name,
|
|
'type': render_type(arg.type),
|
|
'description': arg.description,
|
|
'since': arg.since or None,
|
|
'deprecated': arg.deprecated or None,
|
|
'optional': optional,
|
|
'default': None,
|
|
}
|
|
|
|
if isinstance(arg, VarArgs):
|
|
data.update({
|
|
'min': str(arg.min_varargs) if arg.min_varargs > 0 else '0',
|
|
'max': str(arg.max_varargs) if arg.max_varargs > 0 else 'infinity',
|
|
})
|
|
if isinstance(arg, (Kwarg, PosArg)):
|
|
data.update({'default': arg.default or None})
|
|
if isinstance(arg, Kwarg):
|
|
data.update({'required': arg.required})
|
|
return data
|
|
|
|
mname = f'\\{func.name}' if func.name == '[index]' else func.name
|
|
|
|
data: FunctionDictType = {
|
|
'name': f'{func.obj.name}.{mname}' if isinstance(func, Method) else func.name,
|
|
'base_level': '##' if isinstance(func, Method) else '#',
|
|
'type_name_upper': 'Method' if isinstance(func, Method) else 'Function',
|
|
'type_name': 'method' if isinstance(func, Method) else 'function',
|
|
'description': func.description,
|
|
'notes': func.notes,
|
|
'warnings': func.warnings,
|
|
'example': func.example or None,
|
|
'signature_level': 'h4' if isinstance(func, Method) else 'h3',
|
|
'signature': render_signature(),
|
|
'has_args': bool(func.posargs or func.optargs or func.kwargs or func.varargs),
|
|
# Merge posargs and optargs by generating the *[optional]* tag for optargs
|
|
'posargs': {
|
|
'args': [gen_arg_data(x) for x in func.posargs] + [gen_arg_data(x, optional=True) for x in func.optargs]
|
|
} if func.posargs or func.optargs else None,
|
|
'kwargs': {'args': [gen_arg_data(x) for x in self.sorted_and_filtered(list(func.kwargs.values()))]} if func.kwargs else None,
|
|
'varargs': gen_arg_data(func.varargs) if func.varargs else None,
|
|
'arg_flattening': func.arg_flattening,
|
|
|
|
# For the feature taggs template
|
|
'since': func.since or None,
|
|
'deprecated': func.deprecated or None,
|
|
'optional': False,
|
|
'default': None
|
|
}
|
|
|
|
return data
|
|
|
|
def _write_object(self, obj: Object) -> None:
|
|
data = {
|
|
'name': obj.name,
|
|
'title': obj.long_name if obj.obj_type == ObjectType.RETURNED else obj.name,
|
|
'description': obj.description,
|
|
'notes': obj.notes,
|
|
'warnings': obj.warnings,
|
|
'long_name': obj.long_name,
|
|
'obj_type_name': _OBJ_ID_MAP[obj.obj_type].capitalize(),
|
|
'example': obj.example or None,
|
|
'has_methods': bool(obj.methods),
|
|
'has_inherited_methods': bool(obj.inherited_methods),
|
|
'has_subclasses': bool(obj.extended_by),
|
|
'is_returned': bool(obj.returned_by),
|
|
'extends': obj.extends_obj.name if obj.extends_obj else None,
|
|
'returned_by': [self._link_to_object(x) for x in self.sorted_and_filtered(obj.returned_by)],
|
|
'extended_by': [self._link_to_object(x) for x in self.sorted_and_filtered(obj.extended_by)],
|
|
'methods': [self._gen_func_or_method(m) for m in self.sorted_and_filtered(obj.methods)],
|
|
'inherited_methods': [self._gen_func_or_method(m) for m in self.sorted_and_filtered(obj.inherited_methods)],
|
|
}
|
|
|
|
self._write_template(data, self._gen_object_file_id(obj), 'object')
|
|
|
|
def _write_functions(self) -> None:
|
|
data = {'functions': [self._gen_func_or_method(x) for x in self.functions]}
|
|
self._write_template(data, 'root.functions')
|
|
|
|
def _root_refman_docs(self) -> None:
|
|
def gen_obj_links(objs: T.List[Object]) -> T.List[T.Dict[str, str]]:
|
|
ret: T.List[T.Dict[str, str]] = []
|
|
for o in objs:
|
|
ret += [{'indent': '', 'link': self._link_to_object(o), 'brief': self.brief(o)}]
|
|
for m in self.sorted_and_filtered(o.methods):
|
|
ret += [{'indent': ' ', 'link': self._link_to_object(m), 'brief': self.brief(m)}]
|
|
if o.obj_type == ObjectType.MODULE and self.extract_returned_by_module(o):
|
|
tmp = gen_obj_links(self.extract_returned_by_module(o))
|
|
tmp = [{**x, 'indent': ' ' + x['indent']} for x in tmp]
|
|
ret += [{'indent': ' ', 'link': '**New objects:**', 'brief': ''}]
|
|
ret += [*tmp]
|
|
return ret
|
|
|
|
data = {
|
|
'root': self._gen_filename('root'),
|
|
'elementary': gen_obj_links(self.elementary),
|
|
'returned': gen_obj_links(self.returned),
|
|
'builtins': gen_obj_links(self.builtins),
|
|
'modules': gen_obj_links(self.modules),
|
|
'functions': [{'indent': '', 'link': self._link_to_object(x), 'brief': self.brief(x)} for x in self.functions],
|
|
'enable_modules': self.enable_modules,
|
|
}
|
|
|
|
dummy = {'root': self._gen_filename('root')}
|
|
|
|
self._write_template(data, 'root')
|
|
self._write_template({**dummy, 'name': 'Elementary types'}, f'root.{_OBJ_ID_MAP[ObjectType.ELEMENTARY]}', 'dummy')
|
|
self._write_template({**dummy, 'name': 'Builtin objects'}, f'root.{_OBJ_ID_MAP[ObjectType.BUILTIN]}', 'dummy')
|
|
self._write_template({**dummy, 'name': 'Returned objects'}, f'root.{_OBJ_ID_MAP[ObjectType.RETURNED]}', 'dummy')
|
|
|
|
if self.enable_modules:
|
|
self._write_template({**dummy, 'name': 'Modules'}, f'root.{_OBJ_ID_MAP[ObjectType.MODULE]}', 'dummy')
|
|
|
|
|
|
def generate(self) -> None:
|
|
mlog.log('Generating markdown files...')
|
|
with mlog.nested():
|
|
self._write_functions()
|
|
for obj in self.objects:
|
|
if not self.enable_modules and (obj.obj_type == ObjectType.MODULE or obj.defined_by_module is not None):
|
|
continue
|
|
self._write_object(obj)
|
|
self._root_refman_docs()
|
|
self._configure_sitemap()
|
|
self._generate_link_def()
|
|
|
|
def _configure_sitemap(self) -> None:
|
|
'''
|
|
Replaces the `@REFMAN_PLACEHOLDER@` placeholder with the reference
|
|
manual sitemap. The structure of the sitemap is derived from the
|
|
file IDs.
|
|
'''
|
|
raw = self.sitemap_in.read_text(encoding='utf-8')
|
|
out = ''
|
|
for l in raw.split('\n'):
|
|
if '@REFMAN_PLACEHOLDER@' not in l:
|
|
out += f'{l}\n'
|
|
continue
|
|
mlog.log('Generating', mlog.bold(self.sitemap_out.as_posix()))
|
|
base_indent = l.replace('@REFMAN_PLACEHOLDER@', '')
|
|
for k in sorted(self.generated_files.keys()):
|
|
indent = base_indent + '\t' * k.count('.')
|
|
out += f'{indent}{self.generated_files[k]}\n'
|
|
self.sitemap_out.write_text(out, encoding='utf-8')
|
|
|
|
def _generate_link_def(self) -> None:
|
|
'''
|
|
Generate the link definition file for the refman_links hotdoc
|
|
plugin. The plugin is then responsible for replacing the [[tag]]
|
|
tags with custom HTML elements.
|
|
'''
|
|
data: T.Dict[str, str] = {}
|
|
|
|
# Objects and methods
|
|
for obj in self.objects:
|
|
obj_file = self._gen_filename(self._gen_object_file_id(obj), extension='html')
|
|
data[f'@{obj.name}'] = obj_file
|
|
for m in obj.methods:
|
|
data[f'{obj.name}.{m.name}'] = f'{obj_file}#{obj.name}{m.name}'
|
|
|
|
# Functions
|
|
funcs_file = self._gen_filename('root.functions', extension='html')
|
|
for fn in self.functions:
|
|
data[fn.name] = f'{funcs_file}#{fn.name}'
|
|
|
|
self.link_def_out.write_text(json.dumps(data, indent=2), encoding='utf-8')
|