blob: ca8f62d19d1e481e556619e0df077c210f9d5e2e [file] [log] [blame]
Jack Rosenthal46e24bc2023-04-17 12:25:41 -06001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Generate documentation for `cros query` types in Markdown."""
6
7import functools
8import io
9from pathlib import Path
10import typing
11from typing import Any, TextIO
12
13from chromite.format import formatters
14from chromite.lib import build_query
15from chromite.lib import commandline
16from chromite.lib import constants
17
18
19_DEFAULT_OUTPUT = constants.CHROMITE_DIR / "docs" / "cros-query-types.md"
20_QUERY_TARGETS = [
21 build_query.Board,
22 build_query.Ebuild,
23 build_query.Profile,
24 build_query.Overlay,
25]
26
27
28def _repr_type(type_: Any) -> str:
29 """Format a data type for the markdown document.
30
31 Args:
32 type_: A typing annotation.
33
34 Returns:
35 A string of how the type should be represented.
36 """
37
38 # In Python 3.8 and earlier, we have to hack around to get the type name.
39 # This is made easier in Python 3.9+, where __name__ is populated correctly.
40 # Once we stop supporting Python 3.8 and earlier, this can be dropped.
41 def _get_type_name():
42 name = getattr(type_, "__name__", None)
43 if name:
44 return name
45
46 # This is hacks for Python 3.8 and earlier only.
47 # pylint: disable=protected-access
48 name = getattr(type_, "_name", None)
49 if name:
50 return name
51
52 return type_.__origin__._name
53
54 name = _get_type_name()
55 args = typing.get_args(type_)
56 if not args:
57 return name
58
59 # Python 3.8 and earlier loose track of Optional[T] and store it as
60 # Union[T, NoneType]. Translate this back for consistent behavior.
61 if name == "Union" and len(args) == 2 and args[1] == type(None):
62 name = "Optional"
63
64 # Optional[T] type is weird, and provides args (T, NoneType). Strip the
65 # excess NoneType.
66 if name == "Optional":
67 args = args[:1]
68
69 return f"{name}[{', '.join(_repr_type(x) for x in args)}]"
70
71
72def _gen_docs(output: TextIO):
73 """Generate the documentation in Markdown format.
74
75 Args:
76 output: The file-like object for the documentation to be written to.
77 """
78
79 def _pr(*args, **kwargs):
80 kwargs.setdefault("file", output)
81 print(*args, **kwargs)
82
83 def _doc_attr(func, call_anno="", type_anno=""):
84 _pr(f"* `{func.__name__}{call_anno}`{type_anno}: {func.__doc__}")
85
86 def _doc_prop(func):
87 return_type = typing.get_type_hints(func).get("return")
88 _doc_attr(func, type_anno=f" (`{_repr_type(return_type)}`)")
89
90 _pr("<!-- This file is auto-generated! Do not edit by hand. -->")
91 _pr("<!-- To update, run chromite/scripts/generate_query_docs. -->")
92 _pr()
93 _pr("# `cros query` Target Types")
94
95 for target in _QUERY_TARGETS:
96 _pr()
97 _pr(f"## {target.__name__}")
98 _pr()
99 _pr("**Attributes:**")
100 _pr()
101
102 for attr in sorted(dir(target)):
103 if attr.startswith("_"):
104 continue
105 # tree() is considered internal to the CLI.
106 if attr == "tree":
107 continue
108 method = getattr(target, attr)
109 if not method.__doc__:
110 continue
111 if isinstance(method, property):
112 _doc_prop(method.fget)
113 elif isinstance(method, functools.cached_property):
114 _doc_prop(method.func)
115 else:
116 hints = typing.get_type_hints(method)
117 return_type = hints.pop("return")
118 args = ", ".join(
119 f"{k}: {_repr_type(v)}" for k, v in hints.items()
120 )
121 call_anno = f"({args}) -> {_repr_type(return_type)}"
122 _doc_attr(method, call_anno=call_anno)
123
124
125def _parse_args(argv):
126 parser = commandline.ArgumentParser(description=__doc__)
127 parser.add_argument(
128 "-o",
129 "--output-file",
130 type=Path,
131 default=_DEFAULT_OUTPUT,
132 help="Path to write markdown",
133 )
134 return parser.parse_args(argv)
135
136
137def main(argv):
138 args = _parse_args(argv)
139 buf = io.StringIO()
140 _gen_docs(buf)
141 contents = formatters.whitespace.Data(buf.getvalue(), path=args.output_file)
142 args.output_file.write_text(contents, encoding="utf-8")