1# This file is part of Hypothesis, which may be found at 2# https://github.com/HypothesisWorks/hypothesis/ 3# 4# Most of this work is copyright (C) 2013-2021 David R. MacIver 5# (david@drmaciver.com), but it contains contributions by others. See 6# CONTRIBUTING.rst for a full list of people who may hold copyright, and 7# consult the git log if you need to determine who owns an individual 8# contribution. 9# 10# This Source Code Form is subject to the terms of the Mozilla Public License, 11# v. 2.0. If a copy of the MPL was not distributed with this file, You can 12# obtain one at https://mozilla.org/MPL/2.0/. 13# 14# END HEADER 15 16""" 17.. _hypothesis-cli: 18 19---------------- 20hypothesis[cli] 21---------------- 22 23:: 24 25 $ hypothesis --help 26 Usage: hypothesis [OPTIONS] COMMAND [ARGS]... 27 28 Options: 29 --version Show the version and exit. 30 -h, --help Show this message and exit. 31 32 Commands: 33 codemod `hypothesis codemod` refactors deprecated or inefficient code. 34 fuzz [hypofuzz] runs tests with an adaptive coverage-guided fuzzer. 35 write `hypothesis write` writes property-based tests for you! 36 37This module requires the :pypi:`click` package, and provides Hypothesis' command-line 38interface, for e.g. :doc:`'ghostwriting' tests <ghostwriter>` via the terminal. 39It's also where `HypoFuzz <https://hypofuzz.com/>`__ adds the :command:`hypothesis fuzz` 40command (`learn more about that here <https://hypofuzz.com/docs/quickstart.html>`__). 41""" 42 43import builtins 44import importlib 45import sys 46from difflib import get_close_matches 47from functools import partial 48from multiprocessing import Pool 49 50try: 51 import pytest 52except ImportError: 53 pytest = None # type: ignore 54 55MESSAGE = """ 56The Hypothesis command-line interface requires the `{}` package, 57which you do not have installed. Run: 58 59 python -m pip install --upgrade hypothesis[cli] 60 61and try again. 62""" 63 64try: 65 import click 66except ImportError: 67 68 def main(): 69 """If `click` is not installed, tell the user to install it then exit.""" 70 sys.stderr.write(MESSAGE.format("click")) 71 sys.exit(1) 72 73 74else: 75 # Ensure that Python scripts in the current working directory are importable, 76 # on the principle that Ghostwriter should 'just work' for novice users. Note 77 # that we append rather than prepend to the module search path, so this will 78 # never shadow the stdlib or installed packages. 79 sys.path.append(".") 80 81 @click.group(context_settings={"help_option_names": ("-h", "--help")}) 82 @click.version_option() 83 def main(): 84 pass 85 86 def obj_name(s: str) -> object: 87 """This "type" imports whatever object is named by a dotted string.""" 88 s = s.strip() 89 try: 90 return importlib.import_module(s) 91 except ImportError: 92 pass 93 if "." not in s: 94 modulename, module, funcname = "builtins", builtins, s 95 else: 96 modulename, funcname = s.rsplit(".", 1) 97 try: 98 module = importlib.import_module(modulename) 99 except ImportError as err: 100 raise click.UsageError( 101 f"Failed to import the {modulename} module for introspection. " 102 "Check spelling and your Python import path, or use the Python API?" 103 ) from err 104 try: 105 return getattr(module, funcname) 106 except AttributeError as err: 107 public_names = [name for name in vars(module) if not name.startswith("_")] 108 matches = get_close_matches(funcname, public_names) 109 raise click.UsageError( 110 f"Found the {modulename!r} module, but it doesn't have a " 111 f"{funcname!r} attribute." 112 + (f" Closest matches: {matches!r}" if matches else "") 113 ) from err 114 115 def _refactor(func, fname): 116 try: 117 with open(fname) as f: 118 oldcode = f.read() 119 except (OSError, UnicodeError) as err: 120 # Permissions or encoding issue, or file deleted, etc. 121 return f"skipping {fname!r} due to {err}" 122 newcode = func(oldcode) 123 if newcode != oldcode: 124 with open(fname, mode="w") as f: 125 f.write(newcode) 126 127 @main.command() # type: ignore # Click adds the .command attribute 128 @click.argument("path", type=str, required=True, nargs=-1) 129 def codemod(path): 130 """`hypothesis codemod` refactors deprecated or inefficient code. 131 132 It adapts `python -m libcst.tool`, removing many features and config options 133 which are rarely relevant for this purpose. If you need more control, we 134 encourage you to use the libcst CLI directly; if not this one is easier. 135 136 PATH is the file(s) or directories of files to format in place, or 137 "-" to read from stdin and write to stdout. 138 """ 139 try: 140 from libcst.codemod import gather_files 141 142 from hypothesis.extra import codemods 143 except ImportError: 144 sys.stderr.write( 145 "You are missing required dependencies for this option. Run:\n\n" 146 " python -m pip install --upgrade hypothesis[codemods]\n\n" 147 "and try again." 148 ) 149 sys.exit(1) 150 151 # Special case for stdin/stdout usage 152 if "-" in path: 153 if len(path) > 1: 154 raise Exception( 155 "Cannot specify multiple paths when reading from stdin!" 156 ) 157 print("Codemodding from stdin", file=sys.stderr) 158 print(codemods.refactor(sys.stdin.read())) 159 return 0 160 161 # Find all the files to refactor, and then codemod them 162 files = gather_files(path) 163 errors = set() 164 if len(files) <= 1: 165 errors.add(_refactor(codemods.refactor, *files)) 166 else: 167 with Pool() as pool: 168 for msg in pool.imap_unordered( 169 partial(_refactor, codemods.refactor), files 170 ): 171 errors.add(msg) 172 errors.discard(None) 173 for msg in errors: 174 print(msg, file=sys.stderr) 175 return 1 if errors else 0 176 177 @main.command() # type: ignore # Click adds the .command attribute 178 @click.argument("func", type=obj_name, required=True, nargs=-1) 179 @click.option( 180 "--roundtrip", 181 "writer", 182 flag_value="roundtrip", 183 help="start by testing write/read or encode/decode!", 184 ) 185 @click.option( 186 "--equivalent", 187 "writer", 188 flag_value="equivalent", 189 help="very useful when optimising or refactoring code", 190 ) 191 @click.option("--idempotent", "writer", flag_value="idempotent") 192 @click.option("--binary-op", "writer", flag_value="binary_operation") 193 # Note: we deliberately omit a --ufunc flag, because the magic() 194 # detection of ufuncs is both precise and complete. 195 @click.option( 196 "--style", 197 type=click.Choice(["pytest", "unittest"]), 198 default="pytest" if pytest else "unittest", 199 help="pytest-style function, or unittest-style method?", 200 ) 201 @click.option( 202 "-e", 203 "--except", 204 "except_", 205 type=obj_name, 206 multiple=True, 207 help="dotted name of exception(s) to ignore", 208 ) 209 def write(func, writer, except_, style): # noqa: D301 # \b disables autowrap 210 """`hypothesis write` writes property-based tests for you! 211 212 Type annotations are helpful but not required for our advanced introspection 213 and templating logic. Try running the examples below to see how it works: 214 215 \b 216 hypothesis write gzip 217 hypothesis write numpy.matmul 218 hypothesis write re.compile --except re.error 219 hypothesis write --equivalent ast.literal_eval eval 220 hypothesis write --roundtrip json.dumps json.loads 221 hypothesis write --style=unittest --idempotent sorted 222 hypothesis write --binary-op operator.add 223 """ 224 # NOTE: if you want to call this function from Python, look instead at the 225 # ``hypothesis.extra.ghostwriter`` module. Click-decorated functions have 226 # a different calling convention, and raise SystemExit instead of returning. 227 if writer is None: 228 writer = "magic" 229 elif writer == "idempotent" and len(func) > 1: 230 raise click.UsageError("Test functions for idempotence one at a time.") 231 elif writer == "roundtrip" and len(func) == 1: 232 writer = "idempotent" 233 elif writer == "equivalent" and len(func) == 1: 234 writer = "fuzz" 235 236 try: 237 from hypothesis.extra import ghostwriter 238 except ImportError: 239 sys.stderr.write(MESSAGE.format("black")) 240 sys.exit(1) 241 242 code = getattr(ghostwriter, writer)(*func, except_=except_ or (), style=style) 243 try: 244 from rich.console import Console 245 from rich.syntax import Syntax 246 247 from hypothesis.utils.terminal import guess_background_color 248 except ImportError: 249 print(code) 250 else: 251 code = Syntax( 252 code, 253 lexer_name="python", 254 background_color="default", 255 theme="default" if guess_background_color() == "light" else "monokai", 256 ) 257 Console().print(code, soft_wrap=True) 258