Skip to content

Commit 5294843

Browse files
nwatson22rv-auditortothtamas28
authored andcommitted
Improve default argument handling (runtimeverification/pyk#916)
Closes runtimeverification/pyk#885. Overhauls the way command line argument parsing and parameter passing is done. When building a pyk-based command-line tool, for each subcommand of the tool, extend `class Command` . This subclass should contain all information about what arguments are accepted by that command (through providing its own arguments or inheriting from other `Options` classes), the default values of those arguments, the name of the command, and the help string for the command, as static fields. The values of those options for a specific invocation of that command are stored as non-static fields of a `Command`. The `Command` subclass contains the code that runs when that command is called in `exec()`. In addition, default values of arguments inherited from other `Options` classes can be overridden in the subclass. The `CLI` class manages the tool's CLI options. It is constructed by passing in the name of every command subclass to be included in the tool. It can then build the whole `ArgumentParser` for the tool and process the arguments to instantiate a new `*Command` of the correct type and with the correct arguments. Advantages: - All information about a subcommand is consolidated into one place - Default values specified in only one place - Commands only have to be listed once, when instantiating `CLI` - Setting up an argument parser with all subcommands is done automatically. - Routing of requested command to its associated execution function is done automatically. --------- Co-authored-by: devops <[email protected]> Co-authored-by: Tamás Tóth <[email protected]>
1 parent 0eb7ea0 commit 5294843

File tree

9 files changed

+883
-509
lines changed

9 files changed

+883
-509
lines changed

pyk/docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
project = 'pyk'
1010
author = 'Runtime Verification, Inc'
1111
copyright = '2024, Runtime Verification, Inc'
12-
version = '0.1.687'
13-
release = '0.1.687'
12+
version = '0.1.688'
13+
release = '0.1.688'
1414

1515
# -- General configuration ---------------------------------------------------
1616
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

pyk/package/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.687
1+
0.1.688

pyk/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyk"
7-
version = "0.1.687"
7+
version = "0.1.688"
88
description = ""
99
authors = [
1010
"Runtime Verification, Inc. <[email protected]>",

pyk/src/pyk/__main__.py

Lines changed: 29 additions & 322 deletions
Original file line numberDiff line numberDiff line change
@@ -1,345 +1,52 @@
11
from __future__ import annotations
22

3-
import json
43
import logging
54
import sys
6-
from argparse import ArgumentParser, FileType
7-
from enum import Enum
8-
from pathlib import Path
95
from typing import TYPE_CHECKING
106

11-
from graphviz import Digraph
12-
13-
from pyk.kast.inner import KInner
14-
from pyk.kore.rpc import ExecuteResult
15-
16-
from .cli.args import KCLIArgs
17-
from .cli.utils import LOG_FORMAT, dir_path, loglevel
18-
from .coverage import get_rule_by_id, strip_coverage_logger
19-
from .cterm import CTerm
20-
from .kast.manip import (
21-
flatten_label,
22-
minimize_rule,
23-
minimize_term,
24-
propagate_up_constraints,
25-
remove_source_map,
26-
split_config_and_constraints,
7+
from .cli.args import LoggingOptions
8+
from .cli.cli import CLI
9+
from .cli.pyk import (
10+
CoverageCommand,
11+
GraphImportsCommand,
12+
JsonToKoreCommand,
13+
KoreToJsonCommand,
14+
PrintCommand,
15+
ProveCommand,
16+
RPCKastCommand,
17+
RPCPrintCommand,
2718
)
28-
from .kast.outer import read_kast_definition
29-
from .kast.pretty import PrettyPrinter
30-
from .kore.parser import KoreParser
31-
from .kore.rpc import StopReason
32-
from .kore.syntax import Pattern, kore_term
33-
from .ktool.kprint import KPrint
34-
from .ktool.kprove import KProve
35-
from .prelude.k import GENERATED_TOP_CELL
36-
from .prelude.ml import is_top, mlAnd, mlOr
19+
from .cli.utils import LOG_FORMAT, loglevel
3720

3821
if TYPE_CHECKING:
39-
from argparse import Namespace
40-
from typing import Any, Final
22+
from typing import Final
4123

4224

4325
_LOGGER: Final = logging.getLogger(__name__)
4426

4527

46-
class PrintInput(Enum):
47-
KORE_JSON = 'kore-json'
48-
KAST_JSON = 'kast-json'
49-
50-
5128
def main() -> None:
5229
# KAST terms can end up nested quite deeply, because of the various assoc operators (eg. _Map_, _Set_, ...).
5330
# Most pyk operations are defined recursively, meaning you get a callstack the same depth as the term.
5431
# This change makes it so that in most cases, by default, pyk doesn't run out of stack space.
5532
sys.setrecursionlimit(10**7)
5633

57-
cli_parser = create_argument_parser()
58-
args = cli_parser.parse_args()
59-
60-
logging.basicConfig(level=loglevel(args), format=LOG_FORMAT)
61-
62-
executor_name = 'exec_' + args.command.lower().replace('-', '_')
63-
if executor_name not in globals():
64-
raise AssertionError(f'Unimplemented command: {args.command}')
65-
66-
execute = globals()[executor_name]
67-
execute(args)
68-
69-
70-
def exec_print(args: Namespace) -> None:
71-
kompiled_dir: Path = args.definition_dir
72-
printer = KPrint(kompiled_dir)
73-
if args.input == PrintInput.KORE_JSON:
74-
_LOGGER.info(f'Reading Kore JSON from file: {args.term.name}')
75-
kore = Pattern.from_json(args.term.read())
76-
term = printer.kore_to_kast(kore)
77-
else:
78-
_LOGGER.info(f'Reading Kast JSON from file: {args.term.name}')
79-
term = KInner.from_json(args.term.read())
80-
if is_top(term):
81-
args.output_file.write(printer.pretty_print(term))
82-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
83-
else:
84-
if args.minimize:
85-
if args.omit_labels != '' and args.keep_cells != '':
86-
raise ValueError('You cannot use both --omit-labels and --keep-cells.')
87-
88-
abstract_labels = args.omit_labels.split(',') if args.omit_labels != '' else []
89-
keep_cells = args.keep_cells.split(',') if args.keep_cells != '' else []
90-
minimized_disjuncts = []
91-
92-
for disjunct in flatten_label('#Or', term):
93-
try:
94-
minimized = minimize_term(disjunct, abstract_labels=abstract_labels, keep_cells=keep_cells)
95-
config, constraint = split_config_and_constraints(minimized)
96-
except ValueError as err:
97-
raise ValueError('The minimized term does not contain a config cell.') from err
98-
99-
if not is_top(constraint):
100-
minimized_disjuncts.append(mlAnd([config, constraint], sort=GENERATED_TOP_CELL))
101-
else:
102-
minimized_disjuncts.append(config)
103-
term = propagate_up_constraints(mlOr(minimized_disjuncts, sort=GENERATED_TOP_CELL))
104-
105-
args.output_file.write(printer.pretty_print(term))
106-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
107-
108-
109-
def exec_rpc_print(args: Namespace) -> None:
110-
kompiled_dir: Path = args.definition_dir
111-
printer = KPrint(kompiled_dir)
112-
input_dict = json.loads(args.input_file.read())
113-
output_buffer = []
114-
115-
def pretty_print_request(request_params: dict[str, Any]) -> list[str]:
116-
output_buffer = []
117-
non_state_keys = set(request_params.keys()).difference(['state'])
118-
for key in non_state_keys:
119-
output_buffer.append(f'{key}: {request_params[key]}')
120-
state = CTerm.from_kast(printer.kore_to_kast(kore_term(request_params['state'])))
121-
output_buffer.append('State:')
122-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
123-
return output_buffer
124-
125-
def pretty_print_execute_response(execute_result: ExecuteResult) -> list[str]:
126-
output_buffer = []
127-
output_buffer.append(f'Depth: {execute_result.depth}')
128-
output_buffer.append(f'Stop reason: {execute_result.reason.value}')
129-
if execute_result.reason == StopReason.TERMINAL_RULE or execute_result.reason == StopReason.CUT_POINT_RULE:
130-
output_buffer.append(f'Stop rule: {execute_result.rule}')
131-
output_buffer.append(
132-
f'Number of next states: {len(execute_result.next_states) if execute_result.next_states is not None else 0}'
133-
)
134-
state = CTerm.from_kast(printer.kore_to_kast(execute_result.state.kore))
135-
output_buffer.append('State:')
136-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
137-
if execute_result.next_states is not None:
138-
next_states = [CTerm.from_kast(printer.kore_to_kast(s.kore)) for s in execute_result.next_states]
139-
for i, s in enumerate(next_states):
140-
output_buffer.append(f'Next state #{i}:')
141-
output_buffer.append(printer.pretty_print(s.kast, sort_collections=True))
142-
return output_buffer
143-
144-
try:
145-
if 'method' in input_dict:
146-
output_buffer.append('JSON RPC request')
147-
output_buffer.append(f'id: {input_dict["id"]}')
148-
output_buffer.append(f'Method: {input_dict["method"]}')
149-
try:
150-
if 'state' in input_dict['params']:
151-
output_buffer += pretty_print_request(input_dict['params'])
152-
else: # this is an "add-module" request, skip trying to print state
153-
for key in input_dict['params'].keys():
154-
output_buffer.append(f'{key}: {input_dict["params"][key]}')
155-
except KeyError as e:
156-
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
157-
exit(1)
158-
else:
159-
if not 'result' in input_dict:
160-
_LOGGER.critical('The input is neither a request not a resonse')
161-
exit(1)
162-
output_buffer.append('JSON RPC Response')
163-
output_buffer.append(f'id: {input_dict["id"]}')
164-
if list(input_dict['result'].keys()) == ['state']: # this is a "simplify" response
165-
output_buffer.append('Method: simplify')
166-
state = CTerm.from_kast(printer.kore_to_kast(kore_term(input_dict['result']['state'])))
167-
output_buffer.append('State:')
168-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
169-
elif list(input_dict['result'].keys()) == ['module']: # this is an "add-module" response
170-
output_buffer.append('Method: add-module')
171-
output_buffer.append('Module:')
172-
output_buffer.append(input_dict['result']['module'])
173-
else:
174-
try: # assume it is an "execute" response
175-
output_buffer.append('Method: execute')
176-
execute_result = ExecuteResult.from_dict(input_dict['result'])
177-
output_buffer += pretty_print_execute_response(execute_result)
178-
except KeyError as e:
179-
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
180-
exit(1)
181-
if args.output_file is not None:
182-
args.output_file.write('\n'.join(output_buffer))
183-
else:
184-
print('\n'.join(output_buffer))
185-
except ValueError as e:
186-
# shorten and print the error message in case kore_to_kast throws ValueError
187-
_LOGGER.critical(str(e)[:200])
188-
exit(1)
189-
190-
191-
def exec_rpc_kast(args: Namespace) -> None:
192-
"""
193-
Convert an 'execute' JSON RPC response to a new 'execute' or 'simplify' request,
194-
copying parameters from a reference request.
195-
"""
196-
reference_request = json.loads(args.reference_request_file.read())
197-
input_dict = json.loads(args.response_file.read())
198-
execute_result = ExecuteResult.from_dict(input_dict['result'])
199-
non_state_keys = set(reference_request['params'].keys()).difference(['state'])
200-
request_params = {}
201-
for key in non_state_keys:
202-
request_params[key] = reference_request['params'][key]
203-
request_params['state'] = {'format': 'KORE', 'version': 1, 'term': execute_result.state.kore.dict}
204-
request = {
205-
'jsonrpc': reference_request['jsonrpc'],
206-
'id': reference_request['id'],
207-
'method': reference_request['method'],
208-
'params': request_params,
209-
}
210-
args.output_file.write(json.dumps(request))
211-
212-
213-
def exec_prove(args: Namespace) -> None:
214-
kompiled_dir: Path = args.definition_dir
215-
kprover = KProve(kompiled_dir, args.main_file)
216-
final_state = kprover.prove(Path(args.spec_file), spec_module_name=args.spec_module, args=args.kArgs)
217-
args.output_file.write(json.dumps(mlOr([state.kast for state in final_state]).to_dict()))
218-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
219-
220-
221-
def exec_graph_imports(args: Namespace) -> None:
222-
kompiled_dir: Path = args.definition_dir
223-
kprinter = KPrint(kompiled_dir)
224-
definition = kprinter.definition
225-
import_graph = Digraph()
226-
graph_file = kompiled_dir / 'import-graph'
227-
for module in definition.modules:
228-
module_name = module.name
229-
import_graph.node(module_name)
230-
for module_import in module.imports:
231-
import_graph.edge(module_name, module_import.name)
232-
import_graph.render(graph_file)
233-
_LOGGER.info(f'Wrote file: {graph_file}')
234-
235-
236-
def exec_coverage(args: Namespace) -> None:
237-
kompiled_dir: Path = args.definition_dir
238-
definition = remove_source_map(read_kast_definition(kompiled_dir / 'compiled.json'))
239-
pretty_printer = PrettyPrinter(definition)
240-
for rid in args.coverage_file:
241-
rule = minimize_rule(strip_coverage_logger(get_rule_by_id(definition, rid.strip())))
242-
args.output.write('\n\n')
243-
args.output.write('Rule: ' + rid.strip())
244-
args.output.write('\nUnparsed:\n')
245-
args.output.write(pretty_printer.print(rule))
246-
_LOGGER.info(f'Wrote file: {args.output.name}')
247-
248-
249-
def exec_kore_to_json(args: Namespace) -> None:
250-
text = sys.stdin.read()
251-
kore = KoreParser(text).pattern()
252-
print(kore.json)
253-
254-
255-
def exec_json_to_kore(args: dict[str, Any]) -> None:
256-
text = sys.stdin.read()
257-
kore = Pattern.from_json(text)
258-
kore.write(sys.stdout)
259-
sys.stdout.write('\n')
260-
261-
262-
def create_argument_parser() -> ArgumentParser:
263-
k_cli_args = KCLIArgs()
264-
265-
definition_args = ArgumentParser(add_help=False)
266-
definition_args.add_argument('definition_dir', type=dir_path, help='Path to definition directory.')
267-
268-
pyk_args = ArgumentParser()
269-
pyk_args_command = pyk_args.add_subparsers(dest='command', required=True)
270-
271-
print_args = pyk_args_command.add_parser(
272-
'print',
273-
help='Pretty print a term.',
274-
parents=[k_cli_args.logging_args, definition_args, k_cli_args.display_args],
275-
)
276-
print_args.add_argument('term', type=FileType('r'), help='Input term (in format specified with --input).')
277-
print_args.add_argument('--input', default=PrintInput.KAST_JSON, type=PrintInput, choices=list(PrintInput))
278-
print_args.add_argument('--omit-labels', default='', nargs='?', help='List of labels to omit from output.')
279-
print_args.add_argument(
280-
'--keep-cells', default='', nargs='?', help='List of cells with primitive values to keep in output.'
281-
)
282-
print_args.add_argument('--output-file', type=FileType('w'), default='-')
283-
284-
rpc_print_args = pyk_args_command.add_parser(
285-
'rpc-print',
286-
help='Pretty-print an RPC request/response',
287-
parents=[k_cli_args.logging_args, definition_args],
288-
)
289-
rpc_print_args.add_argument(
290-
'input_file',
291-
type=FileType('r'),
292-
help='An input file containing the JSON RPC request or response with KoreJSON payload.',
293-
)
294-
rpc_print_args.add_argument('--output-file', type=FileType('w'), default='-')
295-
296-
rpc_kast_args = pyk_args_command.add_parser(
297-
'rpc-kast',
298-
help='Convert an "execute" JSON RPC response to a new "execute" or "simplify" request, copying parameters from a reference request.',
299-
parents=[k_cli_args.logging_args],
300-
)
301-
rpc_kast_args.add_argument(
302-
'reference_request_file',
303-
type=FileType('r'),
304-
help='An input file containing a JSON RPC request to server as a reference for the new request.',
305-
)
306-
rpc_kast_args.add_argument(
307-
'response_file',
308-
type=FileType('r'),
309-
help='An input file containing a JSON RPC response with KoreJSON payload.',
310-
)
311-
rpc_kast_args.add_argument('--output-file', type=FileType('w'), default='-')
312-
313-
prove_args = pyk_args_command.add_parser(
314-
'prove',
315-
help='Prove an input specification (using kprovex).',
316-
parents=[k_cli_args.logging_args, definition_args],
317-
)
318-
prove_args.add_argument('main_file', type=str, help='Main file used for kompilation.')
319-
prove_args.add_argument('spec_file', type=str, help='File with the specification module.')
320-
prove_args.add_argument('spec_module', type=str, help='Module with claims to be proven.')
321-
prove_args.add_argument('--output-file', type=FileType('w'), default='-')
322-
prove_args.add_argument('kArgs', nargs='*', help='Arguments to pass through to K invocation.')
323-
324-
pyk_args_command.add_parser(
325-
'graph-imports',
326-
help='Graph the imports of a given definition.',
327-
parents=[k_cli_args.logging_args, definition_args],
328-
)
329-
330-
coverage_args = pyk_args_command.add_parser(
331-
'coverage',
332-
help='Convert coverage file to human readable log.',
333-
parents=[k_cli_args.logging_args, definition_args],
334-
)
335-
coverage_args.add_argument('coverage_file', type=FileType('r'), help='Coverage file to build log for.')
336-
coverage_args.add_argument('-o', '--output', type=FileType('w'), default='-')
337-
338-
pyk_args_command.add_parser('kore-to-json', help='Convert textual KORE to JSON', parents=[k_cli_args.logging_args])
339-
340-
pyk_args_command.add_parser('json-to-kore', help='Convert JSON to textual KORE', parents=[k_cli_args.logging_args])
341-
342-
return pyk_args
34+
cli = CLI(
35+
[
36+
CoverageCommand,
37+
GraphImportsCommand,
38+
JsonToKoreCommand,
39+
KoreToJsonCommand,
40+
PrintCommand,
41+
ProveCommand,
42+
RPCKastCommand,
43+
RPCPrintCommand,
44+
]
45+
)
46+
command = cli.get_command()
47+
assert isinstance(command, LoggingOptions)
48+
logging.basicConfig(level=loglevel(command), format=LOG_FORMAT)
49+
command.exec()
34350

34451

34552
if __name__ == '__main__':

0 commit comments

Comments
 (0)