Source code for dataclass_wizard.wizard_cli.cli

"""
Entry point for the Wizard CLI tool.
"""
import argparse
import os
import platform
import sys
import textwrap
from gettext import gettext as _
from json import JSONDecodeError
from pathlib import Path
from typing import TextIO, Optional

from .schema import PyCodeGenerator
from ..__version__ import __version__


# Define the top-level parser
parser: argparse.ArgumentParser


[docs] def main(args=None): """ A companion CLI tool for the Dataclass Wizard, which simplifies interaction with the Python `dataclasses` module. """ setup_parser() args = parser.parse_args(args) try: args.func(args) except AttributeError: # A sub-command is not provided. parser.print_help() parser.exit(0)
[docs] def setup_parser(): """Sets up the Wizard CLI parser.""" global parser desc = main.__doc__ py_version = sys.version.split(" ", 1)[0] # create the top-level parser parser = argparse.ArgumentParser(description=desc) # define global flags for the CLI tool parser.add_argument('-V', '--version', action='version', version=f'%(prog)s-cli/{__version__} ' f'Python/{py_version} ' f'{platform.system()}/{platform.release()}', help='Display the version of this tool.') # Commenting these out for now, as they are all currently a "no-op". # parser.add_argument('-v', '--verbose', action='store_true', # help='Enable verbose output') # parser.add_argument('-q', '--quiet', action='store_true') # Add the sub-commands here. subparsers = parser.add_subparsers(help='Supported sub-commands') # create the parser for the "gs" command gs_parser = subparsers.add_parser( 'gen-schema', aliases=['gs'], help='Generates a Python dataclass schema, given a JSON input.') gs_parser.add_argument('in_file', metavar='in-file', nargs='?', type=FileTypeWithExt('r', ext='.json'), help="Path to JSON file. The default assumes the " "input is piped from stdin or '-'", default=sys.stdin) gs_parser.add_argument('out_file', metavar='out-file', nargs='?', type=FileTypeWithExt('w', ext='.py'), help="Path to new Python file. The default is to " "print the output to stdout or '-'", default=sys.stdout) gs_parser.add_argument("-n", "--no-json-file", action="store_true", help='Do not create a separate JSON file. Note ' 'this only applies when the JSON input is ' 'piped in to stdin.') gs_parser.add_argument("-f", "--force-strings", action="store_true", help='Force-resolve strings to inferred Python types. ' 'For example, a string appearing as "TRUE" will ' 'resolve to a `bool` type, instead of the ' 'default `Union[bool, str]`.') gs_parser.add_argument("-x", "--experimental", action="store_true", help='Enable experimental features via a __future__ ' 'import, which allows PEP-585 and PEP-604 ' 'style annotations in Python 3.7+') gs_parser.set_defaults(func=gen_py_schema)
[docs] class FileTypeWithExt(argparse.FileType): """ Extends :class:`argparse.FileType` to add a default file extension if the provided file name is missing one. """ def __init__(self, mode='r', ext=None, bufsize=-1, encoding=None, errors='ignore'): super().__init__(mode, bufsize, encoding, errors) self._ext = ext def __call__(self, string): # the special argument "-" means sys.std{in,out} if string == '-': if 'r' in self._mode: return sys.stdin elif 'w' in self._mode: # pragma: no branch return sys.stdout else: # pragma: no cover msg = _('argument "-" with mode %r') % self._mode raise ValueError(msg) # all other arguments are used as file names ext = os.path.splitext(string)[-1].lower() # Add the file extension, if needed if not ext and self._ext: string += self._ext try: return open(string, self._mode, self._bufsize, self._encoding, self._errors) except OSError as e: message = _("can't open '%s': %s") raise argparse.ArgumentTypeError(message % (string, e))
[docs] def get_div(out_file: TextIO, char='_', line_width=50): """ Returns a formatted line divider to print. """ if out_file.isatty(): try: w = os.get_terminal_size(out_file.fileno()).columns - 2 if w > 0: line_width = w except (ValueError, OSError): # Perhaps not a real terminal after all pass return char * line_width
[docs] def gen_py_schema(args): """ Entry point for the `wiz gen-schema (gs)` command. """ in_file: TextIO = args.in_file out_file: TextIO = args.out_file no_json_file: bool = args.no_json_file force_strings: bool = args.force_strings experimental: bool = args.experimental # Currently these arguments are unused # verbose, quiet = args.verbose, args.quiet # Check if input is piped from stdin. is_stdin: bool = in_file.name == '<stdin>' # Check if output should be displayed to the terminal. is_stdout: bool = out_file.name == '<stdout>' # Read in contents of the JSON string, from stdin or a local file. json_string: str = in_file.read() try: code_gen = PyCodeGenerator(file_contents=json_string, force_strings=force_strings, experimental=experimental) except JSONDecodeError as e: msg = str(e).lower() if is_stdin and ('double quotes' in msg or 'extra data' in msg): # We can provide a more helpful error message in this case. msg = """\ Confirm that double quotes are properly applied. For example, the following syntax is invalid: echo "{"key": "value"}" | wiz gs Instead, wrap the string with single quotes as shown below: echo \'{"key": "value"}\' | wiz gs """ _exit_with_error(out_file, msg=msg) _exit_with_error(out_file, e) except Exception as e: _exit_with_error(out_file, e) else: print('Successfully generated the Python code for the JSON schema.') print(get_div(out_file)) print() if not is_stdout: out_path = Path(out_file.name) # Only create the JSON file if we are piped the input, and the # `--no-json-file / -n` option is not passed in. add_json_file: bool = is_stdin and not no_json_file print(f'Wrote out the Python Code to: {out_path.absolute()}') if add_json_file: json_loc = out_path.with_suffix('.json') json_loc.write_text(json_string) print(f'Saved the JSON Input to: {json_loc.absolute()}') out_file.write(code_gen.py_code)
def _exit_with_error(out_file: TextIO, e: Optional[Exception] = None, msg: Optional[str] = None, line_width=70, indent=' '): """ Prints the error message from an error `e` or an error message `msg` and exits the program. """ msg_header = ('An error{err_cls}was encountered while parsing the JSON ' 'input:') if not msg: msg = str(e) error_lines = [ msg_header.format(err_cls=f' ({type(e).__name__}) ' if e else ' '), get_div(out_file) ] error_lines.extend( textwrap.wrap( textwrap.dedent(msg), width=line_width, initial_indent=indent, subsequent_indent=indent, drop_whitespace=False, replace_whitespace=False, ) ) sys.exit('\n'.join(error_lines)) if __name__ == "__main__": sys.exit(main())