Coverage for src/pytkdocs/cli.py: 95.00%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Why does this file exist, and why not put this in `__main__`?
2#
3# You might be tempted to import things from `__main__` later,
4# but that will cause problems: the code will get executed twice:
5#
6# - When you run `python -m pytkdocs` python will execute
7# `__main__.py` as a script. That means there won't be any
8# `pytkdocs.__main__` in `sys.modules`.
9# - When you import `__main__` it will get executed again (as a module) because
10# there's no `pytkdocs.__main__` in `sys.modules`.
12"""Module that contains the command line application."""
14import argparse
15import json
16import sys
17import traceback
18from contextlib import contextmanager
19from io import StringIO
20from typing import Dict, List, Optional
22from pytkdocs.loader import Loader
23from pytkdocs.objects import Object
24from pytkdocs.serializer import serialize_object
27def process_config(config: dict) -> dict:
28 """
29 Process a loading configuration.
31 The `config` argument is a dictionary looking like this:
33 ```python
34 {
35 "objects": [
36 {"path": "python.dotted.path.to.the.object1"},
37 {"path": "python.dotted.path.to.the.object2"}
38 ]
39 }
40 ```
42 The result is a dictionary looking like this:
44 ```python
45 {
46 "loading_errors": [
47 "message1",
48 "message2",
49 ],
50 "parsing_errors": {
51 "path.to.object1": [
52 "message1",
53 "message2",
54 ],
55 "path.to.object2": [
56 "message1",
57 "message2",
58 ]
59 },
60 "objects": [
61 {
62 "path": "path.to.object1",
63 # other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
64 },
65 {
66 "path": "path.to.object2",
67 # other attributes, see the documentation for `pytkdocs.objects` or `pytkdocs.serializer`
68 },
69 ]
70 }
71 ```
73 Arguments:
74 config: The configuration.
76 Returns:
77 The collected documentation along with the errors that occurred.
78 """
79 collected = []
80 loading_errors = []
81 parsing_errors = {}
83 for obj_config in config["objects"]:
84 path = obj_config.pop("path")
85 members = obj_config.pop("members", set())
87 if isinstance(members, list): 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true
88 members = set(members)
89 loader = Loader(**obj_config)
91 obj = loader.get_object_documentation(path, members)
93 loading_errors.extend(loader.errors)
94 parsing_errors.update(extract_errors(obj))
96 serialized_obj = serialize_object(obj)
97 collected.append(serialized_obj)
99 return {"loading_errors": loading_errors, "parsing_errors": parsing_errors, "objects": collected}
102def process_json(json_input: str) -> dict:
103 """
104 Process JSON input.
106 Simply load the JSON as a Python dictionary, then pass it to [`process_config`][pytkdocs.cli.process_config].
108 Arguments:
109 json_input: The JSON to load.
111 Returns:
112 The result of the call to [`process_config`][pytkdocs.cli.process_config].
113 """
114 return process_config(json.loads(json_input))
117def extract_docstring_parsing_errors(errors: dict, obj: Object) -> None:
118 """
119 Recursion helper.
121 Update the `errors` dictionary by side-effect. Recurse on the object's children.
123 Arguments:
124 errors: The dictionary to update.
125 obj: The object.
126 """
127 if hasattr(obj, "docstring_errors") and obj.docstring_errors: # noqa: WPS421 (hasattr) 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true
128 errors[obj.path] = obj.docstring_errors
129 for child in obj.children:
130 extract_docstring_parsing_errors(errors, child)
133def extract_errors(obj: Object) -> dict:
134 """
135 Extract the docstring parsing errors of each object, recursively, into a flat dictionary.
137 Arguments:
138 obj: An object from `pytkdocs.objects`.
140 Returns:
141 A flat dictionary. Keys are the objects' names.
142 """
143 parsing_errors: Dict[str, List[str]] = {}
144 extract_docstring_parsing_errors(parsing_errors, obj)
145 return parsing_errors
148def get_parser() -> argparse.ArgumentParser:
149 """
150 Return the program argument parser.
152 Returns:
153 The argument parser for the program.
154 """
155 parser = argparse.ArgumentParser(prog="pytkdocs")
156 parser.add_argument(
157 "-1",
158 "--line-by-line",
159 action="store_true",
160 dest="line_by_line",
161 help="Process each line read on stdin, one by one.",
162 )
163 return parser
166@contextmanager
167def discarded_stdout():
168 """
169 Discard standard output.
171 Yields:
172 Nothing: We only yield to act as a context manager.
173 """
174 # Discard things printed at import time to avoid corrupting our JSON output
175 # See https://github.com/pawamoy/pytkdocs/issues/24
176 old_stdout = sys.stdout
177 sys.stdout = StringIO()
179 yield
181 # Flush imported modules' output, and restore true sys.stdout
182 sys.stdout.flush()
183 sys.stdout = old_stdout
186def main(args: Optional[List[str]] = None) -> int:
187 """
188 Run the main program.
190 This function is executed when you type `pytkdocs` or `python -m pytkdocs`.
192 Arguments:
193 args: Arguments passed from the command line.
195 Returns:
196 An exit code.
197 """
198 parser = get_parser()
199 parsed_args: argparse.Namespace = parser.parse_args(args)
201 if parsed_args.line_by_line:
202 for line in sys.stdin:
203 with discarded_stdout():
204 try:
205 output = json.dumps(process_json(line))
206 except Exception as error: # noqa: W0703 (we purposely catch everything)
207 # Don't fail on error. We must handle the next inputs.
208 # Instead, print error as JSON.
209 output = json.dumps({"error": str(error), "traceback": traceback.format_exc()})
210 print(output) # noqa: WPS421 (we need to print at some point)
211 else:
212 with discarded_stdout():
213 output = json.dumps(process_json(sys.stdin.read()))
214 print(output) # noqa: WPS421 (we need to print at some point)
216 return 0