forked from Breakthrough/PySceneDetect
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerate_cli_docs.py
More file actions
275 lines (210 loc) · 9.11 KB
/
Copy pathgenerate_cli_docs.py
File metadata and controls
275 lines (210 loc) · 9.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# Generate formatted CLI documentation for PySceneDetect.
#
# Inspired by sphinx-click: https://github.com/click-contrib/sphinx-click
#
# Copyright (C) 2014-2024 Brandon Castellano <http://www.bcastell.com>.
# PySceneDetect is licensed under the BSD 3-Clause License; see the
# included LICENSE file, or visit one of the above pages for details.
"""Generates CLI reference documentation file docs/cli.rst.
Run from main repo folder as working directory."""
import inspect
import os
import re
import sys
import typing as ty
from dataclasses import dataclass
# Add parent folder to path so we can resolve `scenedetect` imports.
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
# Third-party imports
import click
from scenedetect._cli import scenedetect
StrGenerator = ty.Generator[str, None, None]
INDENT = " " * 4
PAGE_SEP = "*" * 72
TITLE_SEP = "=" * 72
HEADING_SEP = "-" * 72
OPTION_HELP_OVERRIDES = {
"scenedetect": {
"config": "Path to config file. See :ref:`config file reference <scenedetect_cli-config_file>` for details."
},
}
TITLE_LEVELS = ["*", "=", "-"]
INFO_COMMANDS = ["help", "about", "version"]
INFO_COMMAND_OVERRIDE = """
.. _command-help:
``help``, ``version``, and ``about``
=======================================================================
.. program:: scenedetect help
``scenedetect --help`` will print PySceneDetect options, commands, and examples. You can also specify:
* ``scenedetect [command] --help`` to show options and examples *for* a command or detector
* ``scenedetect help`` command to print full reference of all options, commands, and examples
.. program:: scenedetect version
``scenedetect version`` prints the version of PySceneDetect that is installed, as well as system dependencies.
.. program:: scenedetect about
``scenedetect about`` prints PySceneDetect copyright, licensing, and redistribution information. This includes a list of all third-party software components that PySceneDetect uses or interacts with, as well as a reference to the license and copyright information for each component.
"""
def patch_help(s: str, commands: ty.List[str]) -> str:
# Patch some TODOs still not handled correctly below.
pos = 0
while True:
pos = s.find("global option :option:", pos)
if pos < 0:
break
pos = s.find("<-", pos)
assert pos > 0
s = s[: pos + 1] + "scenedetect " + s[pos + 1 :]
for command in [command for command in commands if command not in INFO_COMMANDS]:
def add_link(_match: re.Match) -> str:
return ":ref:`%s <command-%s>`" % (command, command)
s = re.sub("``%s``(?!\\n)" % command, add_link, s)
return s
def generate_title(s: str, level: int = 0, len: int = 72) -> StrGenerator:
yield "\n"
if level == 0:
yield TITLE_LEVELS[level] * len + "\n"
yield s + "\n"
yield TITLE_LEVELS[level] * len + "\n\n"
@dataclass
class ReplaceWithReference:
range: ty.Tuple[int, int]
ref: str
ref_type: str
def transform_backquotes(s: str) -> str:
return s.replace("``", "`").replace("`", "``")
def add_backquotes(match: re.Match) -> str:
return "``%s``" % match.string[match.start() : match.end()]
def add_backquotes_with_refs(refs: ty.Set[str]) -> ty.Callable[[str], str]:
"""Returns a transformation function that backquotes command examples, adding backquotes and
references to any found options."""
def _add_backquotes(s: re.Match) -> str:
to_add: str = s.string[s.start() : s.end()]
flag = re.search("-+[\w-]+[^\.\=\s\/]*", to_add)
if flag is not None and flag.string[flag.start() : flag.end()] in refs:
# add cross reference
cross_ref = flag.string[flag.start() : flag.end()]
option = s.string[s.start() : s.end()]
return ":option:`%s <%s>`" % (option, cross_ref)
else:
return add_backquotes(s)
return _add_backquotes
def extract_default_value(s: str) -> ty.Tuple[str, ty.Optional[str]]:
default = re.search("\[default: .*\]", s)
if default is not None:
span = default.span()
assert span[1] == len(s)
s, default = s[: span[0]].strip(), s[span[0] : span[1]][len("[default: ") : -1]
# Double-quote any default values that contain spaces.
if " " in default and '"' not in default and "," not in default:
default = '"%s"' % default
return (s, default)
def transform_add_option_refs(s: str, refs: ty.List[str]) -> str:
transform = add_backquotes_with_refs(refs)
# TODO: Match prefix of `global option` and add ref to parent `scenedetect` command option.
# Replace patch to complete this.
# -c/--command
s = re.sub("-\w/--\w[\w-]*", transform, s)
# --arg=value, --arg=1.2.3, --arg=1,2,3
s = re.sub('-+[\w-]+=[^"\s\)]+(?<![\.\,])', transform, s)
# --args=" command with spaces"
s = re.sub('--[\w-]+[=]+".*?"', transform, s)
return s
def format_option(command: click.Command, opt: click.Option, flags: ty.List[str]) -> StrGenerator:
if isinstance(opt, click.Argument):
yield "\n.. option:: %s\n" % opt.name
return
yield "\n.. option:: %s\n" % ", ".join(
arg if opt.metavar is None else "%s %s" % (arg, opt.metavar)
for arg in sorted(opt.opts, reverse=True)
)
help = (
OPTION_HELP_OVERRIDES[command.name][opt.name]
if command.name in OPTION_HELP_OVERRIDES and opt.name in OPTION_HELP_OVERRIDES[command.name]
else opt.help.strip()
)
# TODO: Make metavars link to the option as well.
help, default = extract_default_value(help)
help = transform_add_option_refs(help, flags)
yield "\n %s\n" % help
if default is not None:
yield "\n Default: ``%s``\n" % default
def generate_command_help(
ctx: click.Context, command: click.Command, parent_name: ty.Optional[str] = None
) -> StrGenerator:
# TODO: Add references to long options. Requires splitting out examples.
# TODO: Add references to subcommands. Need to add actual refs, since programs can't be ref'd.
# TODO: Handle dollar signs in examples by having both escaped and unescaped versions
yield "\n.. _command-%s:\n" % command.name
yield "\n.. program:: %s\n\n" % (
command.name if parent_name is None else "%s %s" % (parent_name, command.name)
)
if parent_name:
yield from generate_title("``%s``" % command.name, 1)
replacements = [
opt
for opts in [param.opts for param in command.params if hasattr(param, "opts")]
for opt in opts
]
help = command.help
help = help.replace(
"Examples:\n", "".join(generate_title("Examples", 0 if not parent_name else 2))
)
help = help.replace("\b\n", "")
help = help.format(scenedetect="scenedetect", scenedetect_with_video="scenedetect -i video.mp4")
help = transform_backquotes(help)
help = transform_add_option_refs(help, replacements)
for line in help.strip().splitlines():
if line.startswith(INDENT):
indent = line.count(INDENT)
line = line.strip()
yield "%s``%s``\n" % (indent * INDENT, line) if line else "\n"
else:
yield "%s\n" % line
if command.params:
yield "\n"
yield from generate_title("Options", 0 if not parent_name else 2)
for param in command.params:
yield from format_option(command, param, replacements)
yield "\n"
def generate_subcommands(ctx: click.Context, commands: ty.List[str]) -> StrGenerator:
processed = set()
for info_command in INFO_COMMANDS:
assert info_command in commands
processed.add(info_command)
yield INFO_COMMAND_OVERRIDE
yield from generate_title("Detectors", 0)
detectors = [command for command in commands if command.startswith("detect-")]
for detector in detectors:
yield from generate_command_help(ctx, ctx.command.get_command(ctx, detector), ctx.info_name)
processed.add(detector)
yield from generate_title("Commands", 0)
output_commands = [
command
for command in commands
if (not command.startswith("detect-") and command not in INFO_COMMANDS)
]
for command in output_commands:
yield from generate_command_help(ctx, ctx.command.get_command(ctx, command), ctx.info_name)
processed.add(command)
assert set(commands) == processed
def create_help() -> ty.Tuple[str, ty.List[str]]:
ctx = click.Context(scenedetect, info_name=scenedetect.name)
commands: ty.List[str] = ctx.command.list_commands(ctx)
# ctx.to_info_dict lacks metavar so we have to use the context directly.
actions = [
generate_title("``scenedetect`` 🎬 Command", level=0),
generate_command_help(ctx, ctx.command),
generate_subcommands(ctx, commands),
]
lines = []
for action in actions:
lines.extend(action)
return "".join(lines), commands
def main():
help, commands = create_help()
help = patch_help(help, commands)
with open("docs/cli.rst", "wb") as f:
f.write(help.encode())
if __name__ == "__main__":
main()