diff --git a/.gdbinit b/.gdbinit index afbe69c..7fc0490 100644 --- a/.gdbinit +++ b/.gdbinit @@ -9,4 +9,3 @@ end define hook-run refresh end - diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e78ec4f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,34 @@ +on: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest pre-commit + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + + - name: Pre-commit checks + run: | + pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..afad5a9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-docstring-first + - id: check-json + - id: check-added-large-files +# - id: check-yaml + - id: debug-statements +# - id: name-tests-test + - id: double-quote-string-fixer + - id: requirements-txt-fixer +- repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: [--max-line-length=131] + additional_dependencies: + - flake8-typing-imports==1.12.0 + - flake8-bugbear + - flake8-comprehensions + - flake8-simplify +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 + hooks: + - id: autopep8 +- repo: https://github.com/asottile/reorder_python_imports + rev: v2.6.0 + hooks: + - id: reorder-python-imports + args: [--py37-plus, --add-import, 'from __future__ import annotations'] +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma + args: [--py36-plus] +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.0 + hooks: + - id: setup-cfg-fmt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..70df881 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Contributions welcome. + +## Lint + +This project uses [pre-commit](https://pre-commit.com/). + +```bash +python3 -m pip install -U pre-commit + +pre-commit install +``` + +```bash +pre-commit run --all-files +``` diff --git a/README.md b/README.md index 372d675..1943164 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a CLI tool that, given a WIT specification, will correctly interpret and When no WIT file is provided, the arguments will be interpreted as basic types the same way `wasmtime --invoke` works -To facilitate expression of complex types, this tool accepts JSON notation as input, and produces JSON notation as output. For more information, please see the [examples](#examples) section below. +To facilitate expression of complex types, this tool accepts JSON notation as input, and produces JSON notation as output. For more information, please see the [examples](#examples) section below. # Usage You may use this tool locally, or via a [Docker](#building-and-running-the-docker-image) container. @@ -36,7 +36,7 @@ Specifies 0 or more arguments to pass into the Wasm function. Complex arguments *Options:* `-b, --batch BATCHFILE` * Specifies a path to a file containing one or more JSON-formatted inputs to use in place of in-line arguments (see [Batch File Format](batch-file-format), below). - + `-d, --debug` * Starts the Wasm program in GDB. See [Debugging](debugging), below. @@ -108,7 +108,7 @@ Specifies 0 or more arguments to pass into the Wasm function. Complex arguments `-c, --cache CACHEDIR` * Specifies a directory to use for the binding cache. To help save time on repeated runs, `writ` can cache its generated bindings in a directory and re-use them again later. You can specify the location of this directory with this option. - + `-e, --expect EXPECTSTR` * Specifies an expected result to JSON form. If not matched, the program exits with the error code 2. May not be used with -b. @@ -162,11 +162,11 @@ Each entry in the outer-most list represents the arguments for a single call int When a batch file is in use, output will be formatted in a similar way, with each outer list entry corresponding to one record of input. -# Examples +# Examples All of the examples below apply equally to both `writ` and `writ-docker`. ## Simple numeric arguments -This example passes simple numerics as arguments. Due to the simplicity of the +This example passes simple numerics as arguments. Due to the simplicity of the parameter types (all numeric), a WIT file is optional. ```sh bin/writ --wit examples/int/power.wit examples/int/power.wasm power-of 2 3 @@ -253,7 +253,7 @@ Here, we'll test splitting some strings. We use the `--batch` option for this. e ```sh cat< /tmp/writ-test.json -[ +[ ["first_string_to_test", "_"], ["second-string-to-test", "_"], ["third-string_to__test", "_"], @@ -323,4 +323,3 @@ For this, you will need [Docker](https://docs.docker.com/engine/install/) instal ```bash docker build -f docker/Dockerfile -t ghcr.io/singlestore-labs/writ . ``` - diff --git a/bin/writ b/bin/writ index c928fb4..8f320dd 100755 --- a/bin/writ +++ b/bin/writ @@ -14,4 +14,3 @@ if [ ${PY_SUBVER} -lt 9 ] ; then fi exec ${PYTHON3} ${MYDIR}/../src/writ "$@" - diff --git a/bin/writ-docker b/bin/writ-docker index f17fe1b..f2dff2a 100755 --- a/bin/writ-docker +++ b/bin/writ-docker @@ -16,14 +16,14 @@ Arguments: WASMFILE Specifies the path to the Wasm module (.wasm file) FUNCNAME Specifies the Wasm function name to run ARG... Specifies 0 or more arguments to pass into the Wasm function. - Complex arguments may be expressed in JSON format. May not be + Complex arguments may be expressed in JSON format. May not be used with the -b option Options: -a, --debug-args Passes the given string as additional arguments into GDB (only valid with -d) - -b, --batch Specifies a path to a file containing one or more - JSON-formatted inputs to use in place of in-line + -b, --batch Specifies a path to a file containing one or more + JSON-formatted inputs to use in place of in-line arguments (see "Batch File Format", below) -d, --debug Runs the Wasm module in GDB -e, --expect Specifies an expected result in JSON form. If not @@ -37,9 +37,9 @@ Options: Batch File Format: A JSON-formatted file may be passed in lieu of in-line arguments. This file - must consist of either a list of lists or a list of single values. For + must consist of either a list of lists or a list of single values. For example, either of the following forms will work: - + [ [ "John Lennon", OR [ "John Lennon", "Guitar", 1940 ], "Paul McCartney", [ "Paul McCartney", "Bass", 1942 ], @@ -205,4 +205,3 @@ fi "/wasm-dir/${WASM_NAME}" \ "${WASM_FUNC}" \ "$@" - diff --git a/docker/Dockerfile b/docker/Dockerfile index eb32b79..4bfcb16 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -16,4 +16,3 @@ RUN cd wasmtime-py && python3 download-wasmtime.py && python3 setup.py install & RUN cd writ && python3 setup.py install ENTRYPOINT ["/writ/bin/writ"] - diff --git a/docker/docker-run-gdb b/docker/docker-run-gdb index 8ea9dad..26fcb39 100755 --- a/docker/docker-run-gdb +++ b/docker/docker-run-gdb @@ -14,4 +14,3 @@ eval `resize` cd /writ gdb "$@" - diff --git a/examples/expected/expected.h b/examples/expected/expected.h index 3dce880..e7d3ba8 100644 --- a/examples/expected/expected.h +++ b/examples/expected/expected.h @@ -4,15 +4,15 @@ extern "C" { #endif - + #include #include - + typedef struct { char *ptr; size_t len; } power_string_t; - + void power_string_set(power_string_t *ret, const char *s); void power_string_dup(power_string_t *ret, const char *s); void power_string_free(power_string_t *ret); diff --git a/examples/float/power.h b/examples/float/power.h index e17df92..e9cf745 100644 --- a/examples/float/power.h +++ b/examples/float/power.h @@ -4,7 +4,7 @@ extern "C" { #endif - + #include #include float power_power_of(float a, float b); diff --git a/examples/hilbert/hilbert.wit b/examples/hilbert/hilbert.wit index c0e995e..7f7b350 100644 --- a/examples/hilbert/hilbert.wit +++ b/examples/hilbert/hilbert.wit @@ -12,4 +12,3 @@ hilbert-encode: func(input: hilbert-input, ) -> list wit-source-get: func() -> string wit-source-print: func() - diff --git a/examples/int/power.c b/examples/int/power.c index 9c741f8..b1206e5 100644 --- a/examples/int/power.c +++ b/examples/int/power.c @@ -36,4 +36,3 @@ int32_t power_power_of(int32_t base, int32_t exp) { } return res; } - diff --git a/examples/int/power.h b/examples/int/power.h index 06a16f1..977efff 100644 --- a/examples/int/power.h +++ b/examples/int/power.h @@ -4,7 +4,7 @@ extern "C" { #endif - + #include #include int32_t power_power_of(int32_t base, int32_t exp); diff --git a/examples/list_int/sum.h b/examples/list_int/sum.h index 437f552..c7f3bfa 100644 --- a/examples/list_int/sum.h +++ b/examples/list_int/sum.h @@ -4,7 +4,7 @@ extern "C" { #endif - + #include #include typedef struct { diff --git a/examples/list_string/sum.c b/examples/list_string/sum.c index 2c1378b..09e9fdc 100644 --- a/examples/list_string/sum.c +++ b/examples/list_string/sum.c @@ -59,5 +59,3 @@ int32_t sum_sum_length(sum_list_string_t *a) { } return res; } - - diff --git a/examples/list_string/sum.h b/examples/list_string/sum.h index a6e7a25..923b682 100644 --- a/examples/list_string/sum.h +++ b/examples/list_string/sum.h @@ -4,15 +4,15 @@ extern "C" { #endif - + #include #include - + typedef struct { char *ptr; size_t len; } sum_string_t; - + void sum_string_set(sum_string_t *ret, const char *s); void sum_string_dup(sum_string_t *ret, const char *s); void sum_string_free(sum_string_t *ret); diff --git a/examples/record/src/test.rs b/examples/record/src/test.rs index 729c369..0b7be70 100644 --- a/examples/record/src/test.rs +++ b/examples/record/src/test.rs @@ -124,4 +124,4 @@ impl record::Record for Record { b: a.b, } } -} \ No newline at end of file +} diff --git a/examples/sentiment/sentiment.wit b/examples/sentiment/sentiment.wit index 2149c93..e9178d7 100644 --- a/examples/sentiment/sentiment.wit +++ b/examples/sentiment/sentiment.wit @@ -9,4 +9,3 @@ sentiment: func(input: string, ) -> polarity-scores wit-source-get: func() -> string wit-source-print: func() - diff --git a/examples/string/split.cpp b/examples/string/split.cpp index f8becc7..d776906 100644 --- a/examples/string/split.cpp +++ b/examples/string/split.cpp @@ -88,7 +88,7 @@ void split_split_str(split_string_t *phrase, split_string_t *delim, split_list_s } } subs.push_back(std::pair(phr.substr(start), start)); - + // Populate the result. bool err = false; auto res = (split_subphrase_t *) malloc(phr.size() * sizeof(split_subphrase_t)); @@ -118,10 +118,9 @@ void split_split_str(split_string_t *phrase, split_string_t *delim, split_list_s free(res[i].str.ptr); free(res); } - } + } // Per the Canonical ABI contract, free the input pointers. free(phrase->ptr); free(delim->ptr); } - diff --git a/examples/string/split.h b/examples/string/split.h index e386659..12a3fd1 100644 --- a/examples/string/split.h +++ b/examples/string/split.h @@ -4,15 +4,15 @@ extern "C" { #endif - + #include #include - + typedef struct { char *ptr; size_t len; } split_string_t; - + void split_string_set(split_string_t *ret, const char *s); void split_string_dup(split_string_t *ret, const char *s); void split_string_free(split_string_t *ret); diff --git a/examples/test.sh b/examples/test.sh index 3b92611..bfac8f8 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -60,7 +60,7 @@ function run_align { } function run_alloc { - local size=100000 + local size=100000 echo "running alloc test, function alloc-blob with arguments: $size" $writ --wit $wit_path/alloc.wit $test_path/modules/alloc.wasm alloc-blob $size echo "" @@ -68,7 +68,7 @@ function run_alloc { function run_createthread { echo "running createthread test, function create-thread with arguments: " - $writ --wit $wit_path/createthread.wit $test_path/modules/createthread.wasm create-thread + $writ --wit $wit_path/createthread.wit $test_path/modules/createthread.wasm create-thread echo "" } @@ -129,7 +129,7 @@ function run_prime { local a=2 local b=10 echo "running prime test with arguments $a " - $writ $wasm_path/prime.wasm is-prime $a + $writ $wasm_path/prime.wasm is-prime $a echo "running prime test with arguments $b " $writ --wit $wit_path/prime.wit $test_path/modules/prime.wasm is-prime $b echo "" @@ -139,7 +139,7 @@ function run_plus { local a=2 local b=10 echo "running plus test with arguments $a $b" - $writ $wasm_path/plus.wasm plus $a $b + $writ $wasm_path/plus.wasm plus $a $b echo "" } @@ -157,9 +157,9 @@ function run_types-basicabi { echo "running test_f64 test with arguments: $f64" $writ $wasm_path/types-basicabi.wasm test_f64 $f64 echo "running test_noarg test with arguments: " - $writ $wasm_path/types-basicabi.wasm test_noarg + $writ $wasm_path/types-basicabi.wasm test_noarg echo "running test_noret test with arguments: " - $writ $wasm_path/types-basicabi.wasm test_noret + $writ $wasm_path/types-basicabi.wasm test_noret echo "running test_noargnoret test with arguments: " $writ $wasm_path/types-basicabi.wasm test_noargnoret } @@ -175,14 +175,14 @@ function run_tests { # run_mult # run_align # run_alloc - run_deeparg + run_deeparg # run_hilbert # run_filter_users # run_passthru # run_prime -# run_plus +# run_plus # run_types-basicabi -# run_infinite +# run_infinite } run_tests diff --git a/requirements.txt b/requirements.txt index b0c532c..a6c766f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ wasmtime - diff --git a/setup.cfg b/setup.cfg index 2b75a63..b79437c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,8 +25,8 @@ packages = find: install_requires = wasmtime python_requires = >=3.7 -tests_require = - nose2 +tests_require = + nose2 [mypy] check_untyped_defs = true diff --git a/src/error_handler.py b/src/error_handler.py index 3976e05..1c87dad 100644 --- a/src/error_handler.py +++ b/src/error_handler.py @@ -1,14 +1,19 @@ +from __future__ import annotations + import os import sys from enum import Enum + def eprint(s): print(s, file=sys.stderr) + def abort(s): eprint(s) os._exit(1) + class ErrorCode(Enum): INITIALIZE_MODULE_FAILED = 1 INVOKE_INITIALIZE_FAILED = 2 diff --git a/src/json_utils.py b/src/json_utils.py index ad52942..4880396 100644 --- a/src/json_utils.py +++ b/src/json_utils.py @@ -1,45 +1,44 @@ -import error_handler +from __future__ import annotations + import json +import os import sys import typing +import error_handler + def check_and_load(json_args: typing.List[str], index: int) -> str: try: loaded_json = json.loads(json_args[index]) - except: + except json.JSONDecodeError: print( - f"ERROR: Failed to load json object at index {str(index - 1)}. Check your json syntax again.", + f'ERROR: Failed to load json object at index {str(index - 1)}. Check your json syntax again.', file=sys.stderr, ) - sys.exit(1) + os._exit(1) return loaded_json def to_py_obj(pyobj: str) -> typing.Any: - if hasattr(pyobj, "__dict__"): + if hasattr(pyobj, '__dict__'): return {k: to_py_obj(v) for k, v in pyobj.__dict__.items()} - elif isinstance(pyobj, list) or isinstance(pyobj, tuple): + elif isinstance(pyobj, (list, tuple)): return [to_py_obj(x) for x in pyobj] - elif isinstance(pyobj, str): - return pyobj - elif isinstance(pyobj, int) or isinstance(pyobj, float): + elif isinstance(pyobj, (str, int, float)): return pyobj elif isinstance(pyobj, bool): return pyobj.lower() raise error_handler.Error( error_handler.ErrorCode.PYOBJ_TO_PYSTR_FAILED, - f"ERROR: Convert following Python type: {type(pyobj)} to Python str is not implemented", + f'ERROR: Convert following Python type: {type(pyobj)} to Python str is not implemented', ) - sys.exit(1) + os._exit(1) def is_atomic_type(arg: typing.Any) -> bool: return ( - isinstance(arg, int) - or isinstance(arg, float) - or isinstance(arg, str) - or isinstance(arg, bool) + isinstance(arg, (int, float, str, bool)) ) @@ -65,24 +64,25 @@ def process_arg(self, arg: tuple[typing.Any, typing.Any]) -> typing.Any: if is_atomic_type(arg_value): if type(arg_value) is not arg_type: print( - f"ERROR: Type mismatch between {arg_value} and {arg_type}. Check your input again.", + f'ERROR: Type mismatch between {arg_value} and {arg_type}. Check your input again.', file=sys.stderr, ) - sys.exit(1) + os._exit(1) return arg_value elif arg_type is bytes: # if arg type is bytes, arg value type is assumed to be in form of # array of integer [a_1, a_2, ..., a_n], a_i represent each bit i-th - return b"".join([x.to_bytes(1, "big") for x in arg_value]) + return b''.join([x.to_bytes(1, 'big') for x in arg_value]) elif isinstance(arg_value, dict): return self.process_dict_arg(arg_value, arg_type) elif isinstance(arg_value, list): - arg_elem_type = arg_type.__args__[0] # type of each element in the list + # type of each element in the list + arg_elem_type = arg_type.__args__[0] return [self.process_arg((x, arg_elem_type)) for x in arg_value] else: raise error_handler.Error( error_handler.ErrorCode.ARG_TYPE_NOT_IMPLEMENTED, - f"Parsing json for the type: {str(type(arg_value))} is not implemented.", + f'Parsing json for the type: {str(type(arg_value))} is not implemented.', ) def parse_json_args(self, args: list[tuple[typing.Any, type]]) -> list[typing.Any]: diff --git a/src/parse_input.py b/src/parse_input.py index 6d51466..c398b78 100644 --- a/src/parse_input.py +++ b/src/parse_input.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import argparse import os import pwd import sys import tempfile import textwrap -import typing def check_cache_path(cache_path: str) -> str: @@ -19,24 +20,26 @@ def valid_path(arg_path: str): if os.path.exists(arg_path): return os.path.abspath(arg_path) else: - raise argparse.ArgumentTypeError(f"{arg_path} does not exist!") + raise argparse.ArgumentTypeError(f'{arg_path} does not exist!') + class LineWrapRawTextHelpFormatter(argparse.RawDescriptionHelpFormatter): def _split_lines(self, text, width): text = self._whitespace_matcher.sub(' ', text).strip() return textwrap.wrap(text, 55) + def parse(): parser = argparse.ArgumentParser( - usage="%(prog)s [OPTIONS] WASMFILE FUNCNAME [ARGS...]", - description="WASI Reactor Interface Tester", + usage='%(prog)s [OPTIONS] WASMFILE FUNCNAME [ARGS...]', + description='WASI Reactor Interface Tester', formatter_class=LineWrapRawTextHelpFormatter, - epilog=''' + epilog=""" Batch File Format: A JSON-formatted file may be passed in lieu of in-line arguments. This file - must consist of either a list of lists or a list of single values. For + must consist of either a list of lists or a list of single values. For example, either of the following forms will work: - + [ [ "John Lennon", OR [ "John Lennon", "Guitar", 1940 ], "Paul McCartney", [ "Paul McCartney", "Bass", 1942 ], @@ -45,94 +48,104 @@ def parse(): Each entry in the outer-most list represents the arguments for a single call into FUNCNAME. -''', +""", ) parser.add_argument( - "-e", - "--expect", - dest="EXPECTSTR", + '-e', + '--expect', + dest='EXPECTSTR', default=None, required=False, - help="Specifies an expected result in JSON form. If not matched, the program exits with the error code 2. May not be used with -b.", + help='Specifies an expected result in JSON form.' + 'If not matched, the program exits with the error code 2.' + 'May not be used with -b.', ) parser.add_argument( - "-c", - "--cache", - dest="CACHEDIR", + '-c', + '--cache', + dest='CACHEDIR', type=check_cache_path, default=os.path.join( - tempfile.gettempdir(), f"writ-bind-cache-{pwd.getpwuid(os.getuid())[0]}" + tempfile.gettempdir(), + f'writ-bind-cache-{pwd.getpwuid(os.getuid())[0]}', ), required=False, - help="Specifies a directory to use for the binding cache", + help='Specifies a directory to use for the binding cache', ) parser.add_argument( - "-w", - "--wit", - dest="WITFILE", + '-w', + '--wit', + dest='WITFILE', type=valid_path, default=None, required=False, - help="Specifies the path to the WIT (.wit) file", + help='Specifies the path to the WIT (.wit) file', ) parser.add_argument( - "-b", - "--batch", - dest="BATCHFILE", + '-b', + '--batch', + dest='BATCHFILE', type=valid_path, default=None, required=False, - help="Specifies a path to a file containing one or more JSON-formatted inputs to use in place of in-line arguments (see \"Batch File Format\", below)", + help='Specifies a path to a file containing one or more JSON-formatted inputs' + 'to use in place of in-line arguments (see "Batch File Format", below)', ) parser.add_argument( - "-v", - "--verbose", - dest="is_verbose", + '-v', + '--verbose', + dest='is_verbose', default=False, required=False, - action="store_true", - help="Enable debug output", + action='store_true', + help='Enable debug output', ) parser.add_argument( - "-g", - "--debug-info", - dest="is_debug_info", + '-g', + '--debug-info', + dest='is_debug_info', default=False, required=False, - action="store_true", - help="Generate runtime debugging information for module (module must also be compiled in debug mode)", + action='store_true', + help='Generate runtime debugging information for module (module must also be compiled in debug mode)', ) parser.add_argument( - "-q", - "--quiet", - dest="is_quiet", + '-q', + '--quiet', + dest='is_quiet', default=False, required=False, - action="store_true", - help="Suppress result output", + action='store_true', + help='Suppress result output', ) parser.add_argument( - dest="WASMFILE", - help="Specifies the path to the Wasm module (.wasm file)" + dest='WASMFILE', + help='Specifies the path to the Wasm module (.wasm file)', ) parser.add_argument( - dest="FUNCNAME", - help="Specifies the name of the Wasm function to run" + dest='FUNCNAME', + help='Specifies the name of the Wasm function to run', ) parser.add_argument( - dest="ARGS", + dest='ARGS', nargs=argparse.REMAINDER, - help="Specifies 0 or more arguments to pass into the Wasm function. Complex arguments may be expressed in JSON format. May not be used with the -b option", + help='Specifies 0 or more arguments to pass into the Wasm function. ' + 'Complex arguments may be expressed in JSON format. May not be used with the -b option', ) args = parser.parse_args() if args.BATCHFILE and len(args.ARGS) > 0: - print("ERROR: Batch input (-b) may not be specified with in-line input.", file=sys.stderr) + print( + 'ERROR: Batch input (-b) may not be specified with in-line input.', + file=sys.stderr, + ) parser.print_help() os._exit(1) if args.BATCHFILE and args.EXPECTSTR is not None: - print("ERROR: Batch input (-b) may not be specified with an expected result (-e).", file=sys.stderr) + print( + 'ERROR: Batch input (-b) may not be specified with an expected result (-e).', + file=sys.stderr, + ) parser.print_help() os._exit(1) return args - diff --git a/src/validate.py b/src/validate.py index b13e406..0d8ed3c 100644 --- a/src/validate.py +++ b/src/validate.py @@ -1,27 +1,31 @@ """ validate path, user input, etc """ -from enum import Enum -import error_handler +from __future__ import annotations + import filecmp import os import shutil import subprocess import tempfile import typing +from enum import Enum + +import error_handler import validate class ErrorCode(Enum): - EMPTY_STRING = "Empty string or string not found." - VARIABLE_DOES_NOT_EXIST = "Missing the following variable in PATH: " - PATH_NOT_EXECUTABLE = "The following path is not executable: " + EMPTY_STRING = 'Empty string or string not found.' + VARIABLE_DOES_NOT_EXIST = 'Missing the following variable in PATH: ' + PATH_NOT_EXECUTABLE = 'The following path is not executable: ' def resolve_string(s: typing.Optional[str]) -> str: if s is None: raise error_handler.Error( - error_handler.ErrorCode.EMPTY_STRING, f"String {s} is empty or not found." + error_handler.ErrorCode.EMPTY_STRING, + f'String {s} is empty or not found.', ) return str(s) @@ -34,30 +38,35 @@ def check_command(env_name: str, command: str) -> typing.Optional[str]: if path is None: error_handler.Error( error_handler.ErrorCode.VARIABLE_NOT_IN_PATH, - f"{command} does not exist in path.", + f'{command} does not exist in path.', ) if not os.access(os.path.abspath(validate.resolve_string(path)), os.X_OK): error_handler.Error( error_handler.ErrorCode.PATH_NOT_EXECUTABLE, - f"The following path is not executable: {path}", + f'The following path is not executable: {path}', ) return path -def generate_and_move(command: list[str], src_dir: str, dst_path: str, is_verbose: bool): +def generate_and_move( + command: list[str], + src_dir: str, + dst_path: str, + is_verbose: bool, +): try: subprocess.run(command, capture_output=not is_verbose) if is_verbose: - print("Invoking command {}".format(command)) - src_path = os.path.join(src_dir, "bindings.py") + print('Invoking command {}'.format(command)) + src_path = os.path.join(src_dir, 'bindings.py') if is_verbose: - print("Copying {} to {}".format(src_path, dst_path)) + print('Copying {} to {}'.format(src_path, dst_path)) shutil.copy(src_path, dst_path) os.remove(src_path) - except: + except Exception: raise error_handler.Error( error_handler.ErrorCode.UNKNOWN, - "Unknown error when running the command, likely caused by wrong/unmatched wit specification.", + 'Unknown error when running the command, likely caused by wrong/unmatched wit specification.', ) @@ -70,19 +79,21 @@ def check_cached_file_or_generate( is_verbose: bool, ) -> None: if is_verbose: - print("Comparing {} to {}".format(cached_wit_path, wit_path)) + print('Comparing {} to {}'.format(cached_wit_path, wit_path)) if not os.path.exists(cached_wit_path) or not filecmp.cmp( - cached_wit_path, wit_path, shallow=False + cached_wit_path, + wit_path, + shallow=False, ): tmp_dir = tempfile.mkdtemp() try: generate_and_move( [ validate.resolve_string(WIT_BINDGEN_PATH), - "wasmtime-py", - "--export", + 'wasmtime-py', + '--export', wit_path, - "--out-dir", + '--out-dir', tmp_dir, ], tmp_dir, @@ -92,10 +103,10 @@ def check_cached_file_or_generate( generate_and_move( [ validate.resolve_string(WIT_BINDGEN_PATH), - "wasmtime-py", - "--import", + 'wasmtime-py', + '--import', wit_path, - "--out-dir", + '--out-dir', tmp_dir, ], tmp_dir, diff --git a/src/writ b/src/writ index 23e91bc..d5672e4 100755 --- a/src/writ +++ b/src/writ @@ -1,23 +1,24 @@ #!/usr/bin/env python3 -from typing import Optional -import contextlib -import error_handler +from __future__ import annotations + import importlib import inspect import json -import json_utils import os -import parse_input import re -import subprocess import sys import textwrap import typing +from typing import Optional + +import error_handler +import json_utils +import parse_input import validate import wasmtime - from error_handler import abort + class Imports: wit_name: Optional[str] wasm_file: str @@ -29,7 +30,7 @@ class Imports: def __init__(self, args) -> None: cache_path = args.CACHEDIR batch_path = args.BATCHFILE - wit_path = args.WITFILE + wit_path = args.WITFILE self.wasm_file = args.WASMFILE self.func = args.FUNCNAME @@ -41,48 +42,54 @@ class Imports: self.batch = None self.batch_ctr = -1 if batch_path is not None: - assert(len(self.args) == 0) - f = None + assert len(self.args) == 0 try: - try: - f = open(batch_path) - except Exception as e: - abort(f"ERROR: Could not open batch file '{batch_path}': {e}") - assert(f) - - # We risk running out of memory with this approach, but the - # easy workaround is just to break batch files into separate - # parts, so I'm going to take the easy way out right now. - try: - self.batch = json.load(f) - except MemoryError: - abort(f"ERROR: Out of memory while reading batch JSON file; try breaking it into smaller parts.") - except Exception as e: - abort(f"ERROR: Unexpected problem loading JSON file: {e}") - finally: - if f: - f.close() + with open(batch_path) as f: + assert f + # We risk running out of memory with this approach, but the + # easy workaround is just to break batch files into separate + # parts, so I'm going to take the easy way out right now. + try: + self.batch = json.load(f) + except MemoryError: + abort( + 'ERROR: Out of memory while reading batch JSON file; try breaking it into smaller parts.', + ) + except Exception as e: + abort( + f'ERROR: Unexpected problem loading JSON file: {e}', + ) + + except Exception as e: + abort( + f"ERROR: Could not open batch file '{batch_path}': {e}", + ) # Batch input should be a list type, and at least one row. if type(self.batch) is not list: - abort("ERROR: Batch input must be a list of single values, or a list of lists.") + abort( + 'ERROR: Batch input must be a list of single values, or a list of lists.', + ) if self.batch is None or len(self.batch) == 0: - abort(f"ERROR: The batch file '{batch_path}' contained no rows.") + abort( + f"ERROR: The batch file '{batch_path}' contained no rows.", + ) if wit_path is None: self.wit_name = None else: - self.wit_name = re.findall(r"[^\/]+(?=\.)", wit_path)[-1] + self.wit_name = re.findall(r'[^\/]+(?=\.)', wit_path)[-1] # generate bindings - export_name = f"{self.wit_name}_export_bindings" - import_name = f"{self.wit_name}_import_bindings" - export_path = os.path.join(cache_path, f"{export_name}.py") - import_path = os.path.join(cache_path, f"{import_name}.py") - wit_cached_path = os.path.join(cache_path, f"{self.wit_name}.wit") + export_name = f'{self.wit_name}_export_bindings' + import_name = f'{self.wit_name}_import_bindings' + export_path = os.path.join(cache_path, f'{export_name}.py') + import_path = os.path.join(cache_path, f'{import_name}.py') + wit_cached_path = os.path.join(cache_path, f'{self.wit_name}.wit') WIT_BINDGEN_PATH = validate.check_command( - "WRIT_WITBINDGEN_PATH", "wit-bindgen" + 'WRIT_WITBINDGEN_PATH', + 'wit-bindgen', ) validate.check_cached_file_or_generate( @@ -100,7 +107,7 @@ class Imports: def prepare_next_row(self) -> None: if self.batch: - assert(self.batch_ctr + 1 < len(self.batch)) + assert self.batch_ctr + 1 < len(self.batch) self.batch_ctr += 1 if type(self.batch[self.batch_ctr]) is not list: @@ -108,7 +115,7 @@ class Imports: else: self.args = self.batch[self.batch_ctr] - assert(type(self.args) is list) + assert type(self.args) is list def get_types(self, class_name: str) -> tuple[list[typing.Any], typing.Any]: py_import_classes = inspect.getmembers( @@ -116,26 +123,37 @@ class Imports: lambda x: inspect.isclass(x) and x.__name__ == class_name, ) if not py_import_classes: - abort(f"ERROR: Class {class_name} is not found in {self.imported}. Check your wit/wasm fle name or function name and try again.") + abort( + f'ERROR: Class {class_name} is not found in {self.imported}.' + 'Check your wit/wasm fle name or function name and try again.', + ) py_import_class = py_import_classes[0][1] py_funcs = inspect.getmembers( py_import_class, lambda x: inspect.isfunction(x) and x.__name__ == self.func, ) if not py_funcs: - abort(f"ERROR: Function {self.func} is not found in {py_import_class}. Check if your function name is correct or if you have included it.") + abort( + f'ERROR: Function {self.func} is not found in {py_import_class}.' + 'Check if your function name is correct or if you have included it.', + ) py_func = py_funcs[0][1] signatures = inspect.signature(py_func) - prms = [signatures.parameters[x].annotation for x in signatures.parameters.keys()][2:] + prms = [ + signatures.parameters[x].annotation for x in signatures.parameters.keys() + ][2:] ret = signatures.return_annotation return (prms, ret) def run_without_wit_arg( - self, linker: wasmtime.Linker, store: wasmtime.Store, module: wasmtime.Module + self, + linker: wasmtime.Linker, + store: wasmtime.Store, + module: wasmtime.Module, ) -> None: - maybe_func = linker.get(store, "test", self.func) + maybe_func = linker.get(store, 'test', self.func) func: wasmtime.Func if not isinstance(maybe_func, wasmtime.Func): abort(f"ERROR: The symbol '{self.func}' is not a Wasm function.") @@ -144,12 +162,18 @@ class Imports: if self.batch: num_recs = len(self.batch) - rec_info = lambda idx: f" at record {idx+1}" + + def rec_info(idx): + return f' at record {idx+1}' + out_indent = 2 - self.emit("[\n") + self.emit('[\n') else: num_recs = 1 - rec_info = lambda idx: "" + + def rec_info(idx): + return '' + out_indent = 0 # Get the type so we can validate the params. @@ -159,7 +183,9 @@ class Imports: self.prepare_next_row() if len(self.args) != len(ftype.params): - abort(f"ERROR: Argument mismatch{rec_info(i)}. Expected {len(ftype.params)} arguments, but got {len(self.args)}.") + abort( + f'ERROR: Argument mismatch{rec_info(i)}. Expected {len(ftype.params)} arguments, but got {len(self.args)}.', + ) fixed_args = [] for j in range(0, len(self.args)): @@ -169,36 +195,43 @@ class Imports: if expected == expected.i32() or expected == expected.i64(): fixed_args.append(int(self.args[j])) else: - abort(f"ERROR: Wasm function argument type at index {j} is not currently supported by writ.") + abort( + f'ERROR: Wasm function argument type at index {j} is not currently supported by writ.', + ) try: res = func(store, *fixed_args) except Exception as e: - abort(f"ERROR: Invocation of function '{self.func}' failed.\nDetails: {e}") - assert(type(res) is int or type(res) is float) + abort( + f"ERROR: Invocation of function '{self.func}' failed.\nDetails: {e}", + ) + assert type(res) is int or type(res) is float - if self.expected: - if str(res) != self.expected: - if not self.is_quiet: - print("ERROR: Actual result does not match expected:") - print(f"\nExpected:\n{self.expected}\n\nActual:\n{res}") - os._exit(2) + if self.expected and str(res) != self.expected: + if not self.is_quiet: + print('ERROR: Actual result does not match expected:') + print( + f'\nExpected:\n{self.expected}\n\nActual:\n{res}', + ) + os._exit(2) if i > 0: - assert(self.batch) - self.emit(", ") + assert self.batch + self.emit(', ') self.emit(f"{' '*out_indent}{res}\n") if self.batch: - self.emit("]\n") + self.emit(']\n') - def fixup_args(self, types: list[typing.Any], vals: list[str], is_ret: bool) -> list[typing.Any]: - assert(len(vals) == len(types)) + def fixup_args( + self, types: list[typing.Any], vals: list[str], is_ret: bool, + ) -> list[typing.Any]: + assert len(vals) == len(types) fixed = [] for i in range(0, len(vals)): s = vals[i] if types[i] == str: - # Tolerate not surrounding strings with quotes. Add them if a + # Tolerate not surrounding strings with quotes. Add them if a # top-level argument appears without them. if len(s) <= 1 or (s[0] != '"' and s[-1] != '"'): s = '"' + s + '"' @@ -206,50 +239,66 @@ class Imports: # Coerce numeric types into a float representation. try: v = float(s) - except: + except ValueError: if is_ret: - abort(f"ERROR: Expected value could not be converted to a float.") + abort( + 'ERROR: Expected value could not be converted to a float.', + ) else: - abort(f"ERROR: Value at index {i} could not be converted to a float.") + abort( + f'ERROR: Value at index {i} could not be converted to a float.', + ) s = str(v) elif types[i] == int: try: v = int(s) - except: + except ValueError: if is_ret: - abort(f"ERROR: Expected value could not be converted to an int.") + abort( + 'ERROR: Expected value could not be converted to an int.', + ) else: - abort(f"ERROR: Value at index {i} could not be converted to an int.") + abort( + f'ERROR: Value at index {i} could not be converted to an int.', + ) s = str(v) fixed.append(s) return fixed def run_with_wit_arg( - self, linker: wasmtime.Linker, store: wasmtime.Store, module: wasmtime.Module + self, + linker: wasmtime.Linker, + store: wasmtime.Store, + module: wasmtime.Module, ) -> None: resolved_wit_name = validate.resolve_string(self.wit_name) - linker_func_name = "add_" + resolved_wit_name + "_to_linker" + linker_func_name = 'add_' + resolved_wit_name + '_to_linker' linker_func = getattr(self.exported, linker_func_name) linker_func(linker, store, self) - self.func = self.func.replace("-", "_") + self.func = self.func.replace('-', '_') # process arguments - py_class_name = "".join(x.capitalize() for x in resolved_wit_name.split("_")) + py_class_name = ''.join( + x.capitalize() + for x in resolved_wit_name.split('_') + ) (prm_types, ret_type) = self.get_types(py_class_name) expected_json_obj = None if self.expected is not None: fixed_exp = self.fixup_args([ret_type], [self.expected], True)[0] if self.is_verbose: - print("INFO: Fixed up expected: {}".format(fixed_exp)) + print('INFO: Fixed up expected: {}'.format(fixed_exp)) try: expected_json_obj = json.loads(fixed_exp) except Exception as e: - abort(f"ERROR: Expect string was improperly formatted; should be {ret_type}. Error from JSON parser: {e}") - assert(expected_json_obj is not None) + abort( + f'ERROR: Expect string was improperly formatted; should be {ret_type}. Error from JSON parser: {e}', + ) + assert expected_json_obj is not None # call the class in self_imported files that has the same name as the py_class on necessary parameters @@ -260,40 +309,52 @@ class Imports: if self.batch: num_recs = len(self.batch) - rec_info = lambda idx: f" at record {idx+1}" + + def rec_info(idx): + return f' at record {idx+1}' + out_indent = 2 - self.emit("[\n") + self.emit('[\n') else: num_recs = 1 - rec_info = lambda idx: "" + + def rec_info(idx): + return '' + out_indent = 0 for i in range(0, num_recs): self.prepare_next_row() if len(self.args) != len(prm_types): - abort(f"ERROR: Argument mismatch{rec_info(i)}. Expected {len(prm_types)} arguments, but got {len(self.args)}.") + abort( + f'ERROR: Argument mismatch{rec_info(i)}. Expected {len(prm_types)} arguments, but got {len(self.args)}.', + ) result = None try: fixed_args = self.fixup_args(prm_types, self.args, False) if self.is_verbose: - print("INFO: Fixed up arguments: {}".format(fixed_args)) + print('INFO: Fixed up arguments: {}'.format(fixed_args)) json_args = [] for index in range(0, len(fixed_args)): - json_args.append(json_utils.check_and_load(fixed_args, index)) + json_args.append( + json_utils.check_and_load(fixed_args, index), + ) args = parse_args_helper.parse_json_args( - list(zip(json_args, prm_types)) + list(zip(json_args, prm_types)), ) result = getattr(wasm, self.func)(store, *args) except wasmtime._trap.Trap as t: - abort(f'ERROR: Wasm function trapped on the following input{rec_info(i)}:\n{self.args}\n\nDetails:\n{t}') - except: + abort( + f'ERROR: Wasm function trapped on the following input{rec_info(i)}:\n{self.args}\n\nDetails:\n{t}', + ) + except Exception as e: raise error_handler.Error( error_handler.ErrorCode.UNKNOWN, - "Unknown error when running the command.", + f'Unknown error when running the command. {e}', ) - assert(result is not None) + assert result is not None py_obj_out = json_utils.to_py_obj(result) json_str_out = json.dumps(py_obj_out, indent=2) @@ -302,25 +363,28 @@ class Imports: json_obj_out = json.loads(json_str_out) if json_obj_out != expected_json_obj: if not self.is_quiet: - print("ERROR: Actual result does not match expected:") - print(f"\nExpected:\n{self.expected}\n\nActual:\n{json_obj_out}") + print('ERROR: Actual result does not match expected:') + print( + f'\nExpected:\n{self.expected}\n\nActual:\n{json_obj_out}', + ) os._exit(2) if i > 0: - assert(self.batch) - self.emit(",\n") - self.emit(textwrap.indent(json_str_out, " "*out_indent)) + assert self.batch + self.emit(',\n') + self.emit(textwrap.indent(json_str_out, ' ' * out_indent)) if self.batch: if i > 0: - self.emit("\n") - self.emit("]\n") - self.emit("\n") + self.emit('\n') + self.emit(']\n') + self.emit('\n') def emit(self, s: str) -> None: if not self.is_quiet: sys.stdout.write(s) + def run() -> None: args = parse_input.parse() @@ -331,7 +395,7 @@ def run() -> None: config = wasmtime.Config() if args.is_debug_info: config.debug_info = True - config.cranelift_opt_level = "none" + config.cranelift_opt_level = 'none' engine = wasmtime.Engine(config) @@ -342,7 +406,7 @@ def run() -> None: module = wasmtime.Module.from_file(store.engine, imports.wasm_file) linker = wasmtime.Linker(store.engine) linker.define_wasi() - linker.define_module(store, "test", module) + linker.define_module(store, 'test', module) wasi = wasmtime.WasiConfig() wasi.inherit_stdout() wasi.inherit_stderr() @@ -350,23 +414,25 @@ def run() -> None: # invoke _initialize export try: - maybe_func = linker.get(store, "test", "_initialize") + maybe_func = linker.get(store, 'test', '_initialize') if isinstance(maybe_func, wasmtime.Func): maybe_func(store) else: - print("WARNING: _initialize symbol is not a function; skipping invocation.", file=sys.stderr) - except: + print( + 'WARNING: _initialize symbol is not a function; skipping invocation.', + file=sys.stderr, + ) + except Exception: if imports.is_verbose: - print("WARNING: Failed to invoke _initialize.", file=sys.stderr) + print('WARNING: Failed to invoke _initialize.', file=sys.stderr) if imports.wit_name is not None: imports.run_with_wit_arg(linker, store, module) else: imports.run_without_wit_arg(linker, store, module) except Exception as e: - abort(f"ERROR: Failed to invoke wasm module: {e}.") + abort(f'ERROR: Failed to invoke wasm module: {e}.') -if __name__ == "__main__": +if __name__ == '__main__': run() -