Merge pull request #9377 from mensinda/jsonDocs

docs: Add a JSON documentation backend
pull/9445/head
Jussi Pakkanen 3 years ago committed by GitHub
commit 475b8b1ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      .github/workflows/website.yml
  2. 195
      docs/jsonvalidator.py
  3. 11
      docs/markdown/IDE-integration.md
  4. 49
      docs/meson.build
  5. 120
      docs/refman/generatorjson.py
  6. 87
      docs/refman/jsonschema.py
  7. 22
      docs/refman/loaderpickle.py
  8. 11
      docs/refman/main.py
  9. 8
      docs/refman/model.py
  10. 10
      docs/refman/templates/root.mustache

@ -11,7 +11,13 @@ on:
- master
paths:
- docs/**
pull_request:
paths:
- docs/**
workflow_dispatch:
release:
types:
- published
# This job is copy/paster into wrapdb CI, please update it there when doing any
# change here.
@ -40,6 +46,7 @@ jobs:
cd docs
../meson.py setup _build
ninja -C _build
ninja -C _build test
- name: Update website
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
@ -47,3 +54,11 @@ jobs:
cd docs
ninja -C _build upload
if: env.HAS_SSH_KEY == 'true'
- name: Release the current JSON docs
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: docs/_build/reference_manual.json
tag: ${{ github.ref }}
if: ${{ github.event_name == 'release' }}

@ -0,0 +1,195 @@
#!/usr/bin/env python3
# SPDX-License-Identifer: Apache-2.0
# Copyright 2021 The Meson development team
import argparse
import json
from pathlib import Path
from copy import deepcopy
import typing as T
T_None = type(None)
# Global root object
root: dict
def assert_has_typed_keys(path: str, data: dict, keys: T.Dict[str, T.Any]) -> dict:
assert set(data.keys()).issuperset(keys.keys()), f'{path}: DIFF: {set(data.keys()).difference(keys.keys())}'
res = dict()
for key, val in keys.items():
cur = data.pop(key)
assert isinstance(cur, val), f'{path}: type({key}: {cur}) != {val}'
res[key] = cur
return res
def validate_base_obj(path: str, name: str, obj: dict) -> None:
expected: T.Dict[str, T.Any] = {
'name': str,
'description': str,
'since': (str, T_None),
'deprecated': (str, T_None),
'notes': list,
'warnings': list,
}
cur = assert_has_typed_keys(f'{path}.{name}', obj, expected)
assert cur['name'], f'{path}.{name}'
assert cur['description'], f'{path}.{name}'
assert cur['name'] == name, f'{path}.{name}'
assert all(isinstance(x, str) and x for x in cur['notes']), f'{path}.{name}'
assert all(isinstance(x, str) and x for x in cur['warnings']), f'{path}.{name}'
def validate_type(path: str, typ: dict) -> None:
expected: T.Dict[str, T.Any] = {
'obj': str,
'holds': list,
}
cur = assert_has_typed_keys(path, typ, expected)
assert not typ, f'{path} has extra keys: {typ.keys()}'
assert cur['obj'] in root['objects'], path
for i in cur['holds']:
validate_type(path, i)
def validate_arg(path: str, name: str, arg: dict) -> None:
validate_base_obj(path, name, arg)
expected: T.Dict[str, T.Any] = {
'type': list,
'type_str': str,
'required': bool,
'default': (str, T_None),
'min_varargs': (int, T_None),
'max_varargs': (int, T_None),
}
cur = assert_has_typed_keys(f'{path}.{name}', arg, expected)
assert not arg, f'{path}.{name} has extra keys: {arg.keys()}'
assert cur['type'], f'{path}.{name}'
assert cur['type_str'], f'{path}.{name}'
for i in cur['type']:
validate_type(f'{path}.{name}', i)
if cur['min_varargs'] is not None:
assert cur['min_varargs'] > 0, f'{path}.{name}'
if cur['max_varargs'] is not None:
assert cur['max_varargs'] > 0, f'{path}.{name}'
def validate_function(path: str, name: str, func: dict) -> None:
validate_base_obj(path, name, func)
expected: T.Dict[str, T.Any] = {
'returns': list,
'returns_str': str,
'example': (str, T_None),
'posargs': dict,
'optargs': dict,
'kwargs': dict,
'varargs': (dict, T_None),
}
cur = assert_has_typed_keys(f'{path}.{name}', func, expected)
assert not func, f'{path}.{name} has extra keys: {func.keys()}'
assert cur['returns'], f'{path}.{name}'
assert cur['returns_str'], f'{path}.{name}'
for i in cur['returns']:
validate_type(f'{path}.{name}', i)
for k, v in cur['posargs'].items():
validate_arg(f'{path}.{name}', k, v)
for k, v in cur['optargs'].items():
validate_arg(f'{path}.{name}', k, v)
for k, v in cur['kwargs'].items():
validate_arg(f'{path}.{name}', k, v)
if cur['varargs']:
validate_arg(f'{path}.{name}', cur['varargs']['name'], cur['varargs'])
def validate_object(path: str, name: str, obj: dict) -> None:
validate_base_obj(path, name, obj)
expected: T.Dict[str, T.Any] = {
'example': (str, T_None),
'object_type': str,
'methods': dict,
'is_container': bool,
'extends': (str, T_None),
'returned_by': list,
'extended_by': list,
'defined_by_module': (str, T_None),
}
cur = assert_has_typed_keys(f'{path}.{name}', obj, expected)
assert not obj, f'{path}.{name} has extra keys: {obj.keys()}'
for key, val in cur['methods'].items():
validate_function(f'{path}.{name}', key, val)
if cur['extends'] is not None:
assert cur['extends'] in root['objects'], f'{path}.{name}'
assert all(isinstance(x, str) for x in cur['returned_by']), f'{path}.{name}'
assert all(isinstance(x, str) for x in cur['extended_by']), f'{path}.{name}'
assert all(x in root['objects'] for x in cur['extended_by']), f'{path}.{name}'
if cur['defined_by_module'] is not None:
assert cur['defined_by_module'] in root['objects'], f'{path}.{name}'
assert cur['object_type'] == 'RETURNED', f'{path}.{name}'
assert root['objects'][cur['defined_by_module']]['object_type'] == 'MODULE', f'{path}.{name}'
assert name in root['objects_by_type']['modules'][cur['defined_by_module']], f'{path}.{name}'
return
assert cur['object_type'] in {'ELEMENTARY', 'BUILTIN', 'MODULE', 'RETURNED'}, f'{path}.{name}'
if cur['object_type'] == 'ELEMENTARY':
assert name in root['objects_by_type']['elementary'], f'{path}.{name}'
if cur['object_type'] == 'BUILTIN':
assert name in root['objects_by_type']['builtins'], f'{path}.{name}'
if cur['object_type'] == 'RETURNED':
assert name in root['objects_by_type']['returned'], f'{path}.{name}'
if cur['object_type'] == 'MODULE':
assert name in root['objects_by_type']['modules'], f'{path}.{name}'
def main() -> int:
global root
parser = argparse.ArgumentParser(description='Meson JSON docs validator')
parser.add_argument('doc_file', type=Path, help='The JSON docs to validate')
args = parser.parse_args()
root_tmp = json.loads(args.doc_file.read_text(encoding='utf-8'))
root = deepcopy(root_tmp)
assert isinstance(root, dict)
expected: T.Dict[str, T.Any] = {
'version_major': int,
'version_minor': int,
'meson_version': str,
'functions': dict,
'objects': dict,
'objects_by_type': dict,
}
cur = assert_has_typed_keys('root', root_tmp, expected)
assert not root_tmp, f'root has extra keys: {root_tmp.keys()}'
refs = cur['objects_by_type']
expected = {
'elementary': list,
'builtins': list,
'returned': list,
'modules': dict,
}
assert_has_typed_keys(f'root.objects_by_type', refs, expected)
assert not refs, f'root.objects_by_type has extra keys: {refs.keys()}'
assert all(isinstance(x, str) for x in root['objects_by_type']['elementary'])
assert all(isinstance(x, str) for x in root['objects_by_type']['builtins'])
assert all(isinstance(x, str) for x in root['objects_by_type']['returned'])
assert all(isinstance(x, str) for x in root['objects_by_type']['modules'])
assert all(x in root['objects'] for x in root['objects_by_type']['elementary'])
assert all(x in root['objects'] for x in root['objects_by_type']['builtins'])
assert all(x in root['objects'] for x in root['objects_by_type']['returned'])
assert all(x in root['objects'] for x in root['objects_by_type']['modules'])
assert all(root['objects'][x]['object_type'] == 'ELEMENTARY' for x in root['objects_by_type']['elementary'])
assert all(root['objects'][x]['object_type'] == 'BUILTIN' for x in root['objects_by_type']['builtins'])
assert all(root['objects'][x]['object_type'] == 'RETURNED' for x in root['objects_by_type']['returned'])
assert all(root['objects'][x]['object_type'] == 'MODULE' for x in root['objects_by_type']['modules'])
# Check that module references are correct
assert all(all(isinstance(x, str) for x in v) for k, v in root['objects_by_type']['modules'].items())
assert all(all(x in root['objects'] for x in v) for k, v in root['objects_by_type']['modules'].items())
assert all(all(root['objects'][x]['defined_by_module'] == k for x in v) for k, v in root['objects_by_type']['modules'].items())
for key, val in cur['functions'].items():
validate_function('root', key, val)
for key, val in cur['objects'].items():
validate_object('root', key, val)
return 0
if __name__ == '__main__':
raise SystemExit(main())

@ -405,6 +405,17 @@ of a node type or the removal of a key) are unlikely and will be
announced in the release notes.
# JSON Reference manual
In additional to the online [Reference manual](Reference-manual.md), Meson also
offers the manual as JSON file. The content of this file is generated from the
same source as the online documentation. The two versions are thus identical
in content.
This JSON document is attached to every Meson release since *0.60.0*. The JSON
schema is defined by the class structure given in
[`jsonschema.py`](https://github.com/mesonbuild/meson/blob/master/docs/refman/jsonschema.py)
# Existing integrations
- [Gnome Builder](https://wiki.gnome.org/Apps/Builder)

@ -15,27 +15,60 @@ docs_gen = custom_target(
build_by_default: true,
install: false)
refman_gen = custom_target(
'gen_refman',
genrefman = find_program('./genrefman.py')
refman_binary = custom_target(
'gen_refman_bin',
input: files('sitemap.txt'),
output: ['configured_sitemap.txt', 'refman_links.json'],
output: 'reference_manual.bin',
depfile: 'reman_dep.d',
command: [
find_program('./genrefman.py'),
genrefman,
'-l', 'yaml',
'-g', 'pickle',
'-o', '@OUTPUT@',
'--depfile', '@DEPFILE@',
'--force-color',
]
)
refman_md = custom_target(
'gen_refman_md',
input: refman_binary,
output: ['configured_sitemap.txt', 'refman_links.json'],
command: [
genrefman,
'-l', 'pickle',
'-g', 'md',
'-s', '@INPUT@',
'-s', files('sitemap.txt'),
'-i', '@INPUT@',
'-o', '@OUTPUT0@',
'--link-defs', '@OUTPUT1@',
'--depfile', '@DEPFILE@',
'--force-color',
'--no-modules',
],
)
refman_json = custom_target(
'gen_refman_json',
build_by_default: true,
input: refman_binary,
output: 'reference_manual.json',
command: [
genrefman,
'-l', 'pickle',
'-g', 'json',
'-i', '@INPUT@',
'-o', '@OUTPUT@',
'--force-color',
],
)
test('validate_docs', find_program('./jsonvalidator.py'), args: [refman_json])
hotdoc = import('hotdoc')
documentation = hotdoc.generate_doc(meson.project_name(),
project_version: meson.project_version(),
sitemap: refman_gen[0],
sitemap: refman_md[0],
build_by_default: true,
depends: docs_gen,
index: 'markdown/index.md',
@ -49,7 +82,7 @@ documentation = hotdoc.generate_doc(meson.project_name(),
syntax_highlighting_activate: true,
keep_markup_in_code_blocks: true,
extra_extension: meson.current_source_dir() / 'extensions' / 'refman_links.py',
refman_data_file: refman_gen[1],
refman_data_file: refman_md[1],
)
run_target('upload',

@ -0,0 +1,120 @@
# SPDX-License-Identifer: Apache-2.0
# Copyright 2021 The Meson development team
from pathlib import Path
import json
import re
from .generatorbase import GeneratorBase
from . import jsonschema as J
from .model import (
ReferenceManual,
Function,
Object,
Type,
PosArg,
VarArgs,
Kwarg,
)
import typing as T
class GeneratorJSON(GeneratorBase):
def __init__(self, manual: ReferenceManual, out: Path, enable_modules: bool) -> None:
super().__init__(manual)
self.out = out
self.enable_modules = enable_modules
def _generate_type(self, typ: Type) -> T.List[J.Type]:
return [
{
'obj': x.data_type.name,
'holds': self._generate_type(x.holds) if x.holds else [],
}
for x in typ.resolved
]
def _generate_type_str(self, typ: Type) -> str:
# Remove all whitespaces
return re.sub(r'[ \n\r\t]', '', typ.raw)
def _generate_arg(self, arg: T.Union[PosArg, VarArgs, Kwarg], isOptarg: bool = False) -> J.Argument:
return {
'name': arg.name,
'description': arg.description,
'since': arg.since if arg.since else None,
'deprecated': arg.deprecated if arg.deprecated else None,
'type': self._generate_type(arg.type),
'type_str': self._generate_type_str(arg.type),
'required': arg.required if isinstance(arg, Kwarg) else not isOptarg and not isinstance(arg, VarArgs),
'default': arg.default if isinstance(arg, (PosArg, Kwarg)) else None,
'min_varargs': arg.min_varargs if isinstance(arg, VarArgs) and arg.min_varargs > 0 else None,
'max_varargs': arg.max_varargs if isinstance(arg, VarArgs) and arg.max_varargs > 0 else None,
# Not yet supported
'notes': [],
'warnings': [],
}
def _generate_function(self, func: Function) -> J.Function:
return {
'name': func.name,
'description': func.description,
'since': func.since if func.since else None,
'deprecated': func.deprecated if func.deprecated else None,
'notes': func.notes,
'warnings': func.warnings,
'example': func.example if func.example else None,
'returns': self._generate_type(func.returns),
'returns_str': self._generate_type_str(func.returns),
'posargs': {x.name: self._generate_arg(x) for x in func.posargs},
'optargs': {x.name: self._generate_arg(x, True) for x in func.optargs},
'kwargs': {x.name: self._generate_arg(x) for x in self.sorted_and_filtered(list(func.kwargs.values()))},
'varargs': self._generate_arg(func.varargs) if func.varargs else None,
}
def _generate_objects(self, obj: Object) -> J.Object:
return {
'name': obj.name,
'description': obj.description,
'since': obj.since if obj.since else None,
'deprecated': obj.deprecated if obj.deprecated else None,
'notes': obj.notes,
'warnings': obj.warnings,
'defined_by_module': obj.defined_by_module.name if obj.defined_by_module else None,
'object_type': obj.obj_type.name,
'is_container': obj.is_container,
'example': obj.example if obj.example else None,
'extends': obj.extends if obj.extends else None,
'returned_by': [x.name for x in self.sorted_and_filtered(obj.returned_by)],
'extended_by': [x.name for x in self.sorted_and_filtered(obj.extended_by)],
'methods': {x.name: self._generate_function(x) for x in self.sorted_and_filtered(obj.methods)},
}
def _extract_meson_version(self) -> str:
# Hack around python relative imports to get to the Meson version
import sys
sys.path.append(Path(__file__).resolve().parents[2].as_posix())
from mesonbuild.coredata import version
return version
def generate(self) -> None:
data: J.Root = {
'version_major': J.VERSION_MAJOR,
'version_minor': J.VERSION_MINOR,
'meson_version': self._extract_meson_version(),
'functions': {x.name: self._generate_function(x) for x in self.sorted_and_filtered(self.functions)},
'objects': {x.name: self._generate_objects(x) for x in self.sorted_and_filtered(self.objects)},
'objects_by_type': {
'elementary': [x.name for x in self.elementary],
'builtins': [x.name for x in self.builtins],
'returned': [x.name for x in self.returned],
'modules': {
x.name: [y.name for y in self.sorted_and_filtered(self.extract_returned_by_module(x))]
for x in self.modules
},
},
}
self.out.write_text(json.dumps(data), encoding='utf-8')

@ -0,0 +1,87 @@
# SPDX-License-Identifer: Apache-2.0
# Copyright 2021 The Meson development team
import typing as T
# The following variables define the current version of
# the JSON documentation format. This is different from
# the Meson version
VERSION_MAJOR = 1 # Changes here indicate breaking format changes (changes to existing keys)
VERSION_MINOR = 0 # Changes here indicate non-breaking changes (only new keys are added to the existing structure)
class BaseObject(T.TypedDict):
'''
Base object for most dicts in the JSON doc.
All objects inheriting from BaseObject will support
the keys specified here:
'''
name: str
description: str
since: T.Optional[str]
deprecated: T.Optional[str]
notes: T.List[str]
warnings: T.List[str]
class Type(T.TypedDict):
obj: str # References an object from `root.objects`
holds: T.Sequence[object] # Mypy does not support recusive dicts, but this should be T.List[Type]...
class Argument(BaseObject):
'''
Object that represents any type of a single function or method argumet.
'''
type: T.List[Type] # A non-empty list of types that are supported.
type_str: str # Formated version of `type`. Is guranteed to not contain any whitespaces.
required: bool
default: T.Optional[str]
min_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments
max_varargs: T.Optional[int] # Only relevant for varargs, must be `null` for all other types of arguments
class Function(BaseObject):
'''
Represents a function or method.
'''
returns: T.List[Type] # A non-empty list of types that are supported.
returns_str: str # Formated version of `returns`. Is guranteed to not contain any whitespaces.
example: T.Optional[str]
posargs: T.Dict[str, Argument]
optargs: T.Dict[str, Argument]
kwargs: T.Dict[str, Argument]
varargs: T.Optional[Argument]
class Object(BaseObject):
'''
Represents all types of Meson objects. The specific object type is stored in the `object_type` field.
'''
example: T.Optional[str]
object_type: str # Defines the object type: Must be one of: ELEMENTARY, BUILTIN, MODULE, RETURNED
methods: T.Dict[str, Function]
is_container: bool
extends: T.Optional[str]
returned_by: T.List[str]
extended_by: T.List[str]
defined_by_module: T.Optional[str]
class ObjectsByType(T.TypedDict):
'''
References to other objects are stored here for ease of navigation / filtering
'''
elementary: T.List[str]
builtins: T.List[str]
returned: T.List[str]
modules: T.Dict[str, T.List[str]]
class Root(T.TypedDict):
'''
The root object of the JSON reference manual
'''
version_major: int # See the description above for
version_minor: int # VERSION_MAJOR and VERSION_MINOR
meson_version: str
functions: T.Dict[str, Function] # A mapping of <name> to a `Function` object for *all* Meson functions
objects: T.Dict[str, Object] # A mapping of <name> to a `Object` object for *all* Meson objects (including modules, elementary, etc.)
objects_by_type: ObjectsByType

@ -0,0 +1,22 @@
# SPDX-License-Identifer: Apache-2.0
# Copyright 2021 The Meson development team
from pathlib import Path
import pickle
from .loaderbase import LoaderBase
from .model import ReferenceManual
class LoaderPickle(LoaderBase):
def __init__(self, in_file: Path) -> None:
super().__init__()
self.in_file = in_file
def load_impl(self) -> ReferenceManual:
res = pickle.loads(self.in_file.read_bytes())
assert isinstance(res, ReferenceManual)
return res
# Assume that the pickled data is OK and skip validation
def load(self) -> ReferenceManual:
return self.load_impl()

@ -19,9 +19,11 @@ import typing as T
from mesonbuild import mlog
from .loaderbase import LoaderBase
from .loaderpickle import LoaderPickle
from .loaderyaml import LoaderYAML
from .generatorbase import GeneratorBase
from .generatorjson import GeneratorJSON
from .generatorprint import GeneratorPrint
from .generatorpickle import GeneratorPickle
from .generatormd import GeneratorMD
@ -30,10 +32,11 @@ meson_root = Path(__file__).absolute().parents[2]
def main() -> int:
parser = argparse.ArgumentParser(description='Meson reference manual generator')
parser.add_argument('-l', '--loader', type=str, default='yaml', choices=['yaml'], help='Information loader backend')
parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md'], required=True, help='Generator backend')
parser.add_argument('-l', '--loader', type=str, default='yaml', choices=['yaml', 'pickle'], help='Information loader backend')
parser.add_argument('-g', '--generator', type=str, choices=['print', 'pickle', 'md', 'json'], required=True, help='Generator backend')
parser.add_argument('-s', '--sitemap', type=Path, default=meson_root / 'docs' / 'sitemap.txt', help='Path to the input sitemap.txt')
parser.add_argument('-o', '--out', type=Path, required=True, help='Output directory for generated files')
parser.add_argument('-i', '--input', type=Path, default=meson_root / 'docs' / 'yaml', help='Input path for the selected loader')
parser.add_argument('--link-defs', type=Path, help='Output file for the MD generator link definition file')
parser.add_argument('--depfile', type=Path, default=None, help='Set to generate a depfile')
parser.add_argument('--force-color', action='store_true', help='Force enable colors')
@ -44,7 +47,8 @@ def main() -> int:
mlog.colorize_console = lambda: True
loaders: T.Dict[str, T.Callable[[], LoaderBase]] = {
'yaml': lambda: LoaderYAML(meson_root / 'docs' / 'yaml'),
'yaml': lambda: LoaderYAML(args.input),
'pickle': lambda: LoaderPickle(args.input),
}
loader = loaders[args.loader]()
@ -54,6 +58,7 @@ def main() -> int:
'print': lambda: GeneratorPrint(refMan),
'pickle': lambda: GeneratorPickle(refMan, args.out),
'md': lambda: GeneratorMD(refMan, args.out, args.sitemap, args.link_defs, not args.no_modules),
'json': lambda: GeneratorJSON(refMan, args.out, not args.no_modules),
}
generator = generators[args.generator]()

@ -44,20 +44,20 @@ class Type:
# Arguments
@dataclass
class ArgBase(NamedObject):
class ArgBase(NamedObject, FetureCheck):
type: Type
@dataclass
class PosArg(ArgBase, FetureCheck):
class PosArg(ArgBase):
default: str
@dataclass
class VarArgs(ArgBase, FetureCheck):
class VarArgs(ArgBase):
min_varargs: int
max_varargs: int
@dataclass
class Kwarg(ArgBase, FetureCheck):
class Kwarg(ArgBase):
required: bool
default: str

@ -5,8 +5,14 @@ render-subpages: false
# Reference manual
This is the root page of the Meson reference manual. All functions
and methods are documented in detail in the following subpages:
This is the root page of the online Meson reference manual. This
manual is also available in a more machine readable format as a
JSON documented attached to every release since *0.60.0*. See our
[IDE integration](IDE-integration.md) documentation for more
information.
All functions and methods are documented in detail in the
following subpages:
## Elementary types

Loading…
Cancel
Save