Add JSON-compatible and usage message-compatible serialization
Marco Ricci

Marco Ricci commited on 2026-08-16 16:10:07
Zeige 1 geänderte Dateien mit 88 Einfügungen und 0 Löschungen.

... ...
@@ -68,6 +68,17 @@ CLISubcommand: TypeAlias = Union[
68 68
 ]
69 69
 
70 70
 
71
+def _serializer(
72
+    arg: CLIOption
73
+    | CLIOptionGroup
74
+    | CLIArgument
75
+    | CLITerminalSubcommand
76
+    | CLICompositeSubcommand,
77
+    /,
78
+) -> dict[str, Any]:
79
+    return arg.serialize()
80
+
81
+
71 82
 @dataclasses.dataclass(frozen=True)
72 83
 class CLIOption:
73 84
     """A CLI option."""
... ...
@@ -111,6 +122,24 @@ class CLIOption:
111 122
             eager=self.eager,
112 123
         )
113 124
 
125
+    def serialize(self) -> dict[str, Any]:
126
+        return {
127
+            "names": self.names,
128
+            "help": str(self.help),
129
+            "has_argument": self.has_argument,
130
+            "eager": self.eager,
131
+        }
132
+
133
+    def usage(self) -> str:
134
+        name = "/".join(sorted(self.names, key=len)[:1])
135
+        if self.has_argument and self.eager:
136
+            return f"[{name} ARG]!"
137
+        if self.has_argument:
138
+            return f"[{name} ARG]"
139
+        if self.eager:
140
+            return f"[{name}]!"
141
+        return f"[{name}]"
142
+
114 143
 
115 144
 @dataclasses.dataclass(frozen=True)
116 145
 class CLIOptionGroup:
... ...
@@ -123,6 +152,17 @@ class CLIOptionGroup:
123 152
     epilog: TrStr = ""
124 153
     """The epilog."""
125 154
 
155
+    def serialize(self) -> dict[str, Any]:
156
+        return {
157
+            "options": tuple(map(_serializer, self.options)),
158
+            "title": str(self.title),
159
+            "epilog": str(self.epilog),
160
+        }
161
+
162
+    def usage(self) -> str:
163
+        opts = sorted(self.options, key=lambda opt: 0 if opt.eager else 1)
164
+        return " ".join(opt.usage() for opt in opts)
165
+
126 166
 
127 167
 @dataclasses.dataclass(frozen=True)
128 168
 class CLIArgument:
... ...
@@ -133,6 +173,17 @@ class CLIArgument:
133 173
     choices: tuple[str, ...] | None = None
134 174
     """The list of possible values, if applicable."""
135 175
 
176
+    def serialize(self) -> dict[str, Any]:
177
+        return {
178
+            "name": self.name,
179
+            "choices": tuple(self.choices)
180
+            if self.choices is not None
181
+            else None,
182
+        }
183
+
184
+    def usage(self) -> str:
185
+        return self.name
186
+
136 187
 
137 188
 _canon_map_cache: MutableMapping[
138 189
     CLISubcommand, Mapping[str | None, CLIOption | CLISubcommand]
... ...
@@ -296,6 +347,24 @@ class CLITerminalSubcommand:
296 347
             _canon_map_cache[self] = calculate_canon_map(self)
297 348
         return _canon_map_cache[self]
298 349
 
350
+    def serialize(self) -> dict[str, Any]:
351
+        return {
352
+            "names": self.names,
353
+            "contents": tuple(
354
+                tuple(map(_serializer, content)) for content in self.contents
355
+            ),
356
+            "prolog": tuple(map(str, self.prolog)),
357
+            "epilog": tuple(map(str, self.epilog)),
358
+        }
359
+
360
+    def usage(self) -> str:
361
+        tokens = (
362
+            ["/".join(sorted(self.names, key=len))]
363
+            + [group.usage() for group in self.contents[0]]
364
+            + [arg.usage() for arg in self.contents[1]]
365
+        )
366
+        return " ".join(tokens)
367
+
299 368
 
300 369
 @dataclasses.dataclass(frozen=True)
301 370
 class CLICompositeSubcommand:
... ...
@@ -334,6 +403,25 @@ class CLICompositeSubcommand:
334 403
             _canon_map_cache[self] = calculate_canon_map(self)
335 404
         return _canon_map_cache[self]
336 405
 
406
+    def serialize(self) -> dict[str, Any]:
407
+        return {
408
+            "names": self.names,
409
+            "contents": (
410
+                tuple(map(_serializer, self.contents[0])),
411
+                tuple(map(_serializer, self.contents[1])),
412
+                self.contents[2],
413
+            ),
414
+            "prolog": tuple(map(str, self.prolog)),
415
+            "epilog": tuple(map(str, self.epilog)),
416
+        }
417
+
418
+    def usage(self) -> str:
419
+        tokens = ["/".join(sorted(self.names, key=len))]
420
+        tokens.extend(group.usage() for group in self.contents[0])
421
+        canon_subcommand_names = [cmd.names[0] for cmd in self.contents[1]]
422
+        tokens.append("{{{}}}".format("|".join(canon_subcommand_names)))
423
+        return " ".join(tokens)
424
+
337 425
 
338 426
 ParsedCommandLineToken: TypeAlias = tuple[str, ...]
339 427
 ParsedCommandLineSection: TypeAlias = tuple[ParsedCommandLineToken, ...]
340 428