1""" 2This module handles retrieval of configuration values from either the 3command-line arguments or the pyproject.toml file. 4""" 5import argparse 6import pathlib 7import sys 8 9import toml 10 11from .version import __version__ 12 13#: Possible configuration options and their respective defaults 14DEFAULTS = { 15 "min_confidence": 0, 16 "paths": [], 17 "exclude": [], 18 "ignore_decorators": [], 19 "ignore_names": [], 20 "make_whitelist": False, 21 "sort_by_size": False, 22 "verbose": False, 23} 24 25 26def _check_input_config(data): 27 """ 28 Checks the types of the values in *data* against the expected types of 29 config-values. If a value is of the wrong type it will raise a SystemExit. 30 """ 31 for key, value in data.items(): 32 if key not in DEFAULTS: 33 sys.exit(f"Unknown configuration key: {key}") 34 # The linter suggests to use "isinstance" here but this fails to 35 # detect the difference between `int` and `bool`. 36 if type(value) is not type(DEFAULTS[key]): # noqa: E721 37 expected_type = type(DEFAULTS[key]).__name__ 38 sys.exit(f"Data type for {key} must be {expected_type!r}") 39 40 41def _check_output_config(config): 42 """ 43 Run sanity checks on the generated config after all parsing and 44 preprocessing is done. 45 46 Exit the application if an error is encountered. 47 """ 48 if not config["paths"]: 49 sys.exit("Please pass at least one file or directory") 50 51 52def _parse_toml(infile): 53 """ 54 Parse a TOML file for config values. 55 56 It will search for a section named ``[tool.vulture]`` which contains the 57 same keys as the CLI arguments seen with ``--help``. All leading dashes are 58 removed and other dashes are replaced by underscores (so ``--sort-by-size`` 59 becomes ``sort_by_size``). 60 61 Arguments containing multiple values are standard TOML lists. 62 63 Example:: 64 65 [tool.vulture] 66 exclude = ["file*.py", "dir/"] 67 ignore_decorators = ["deco1", "deco2"] 68 ignore_names = ["name1", "name2"] 69 make_whitelist = true 70 min_confidence = 10 71 sort_by_size = true 72 verbose = true 73 paths = ["path1", "path2"] 74 """ 75 data = toml.load(infile) 76 settings = data.get("tool", {}).get("vulture", {}) 77 _check_input_config(settings) 78 return settings 79 80 81def _parse_args(args=None): 82 """ 83 Parse CLI arguments. 84 85 :param args: A list of strings representing the CLI arguments. If left to 86 the default, this will default to ``sys.argv``. 87 """ 88 89 # Sentinel value to distinguish between "False" and "no default given". 90 missing = object() 91 92 def csv(exclude): 93 return exclude.split(",") 94 95 usage = "%(prog)s [options] [PATH ...]" 96 version = f"vulture {__version__}" 97 glob_help = "Patterns may contain glob wildcards (*, ?, [abc], [!abc])." 98 parser = argparse.ArgumentParser(prog="vulture", usage=usage) 99 parser.add_argument( 100 "paths", 101 nargs="*", 102 metavar="PATH", 103 default=missing, 104 help="Paths may be Python files or directories. For each directory" 105 " Vulture analyzes all contained *.py files.", 106 ) 107 parser.add_argument( 108 "--exclude", 109 metavar="PATTERNS", 110 type=csv, 111 default=missing, 112 help=f"Comma-separated list of paths to ignore (e.g.," 113 f' "*settings.py,docs/*.py"). {glob_help} A PATTERN without glob' 114 f" wildcards is treated as *PATTERN*.", 115 ) 116 parser.add_argument( 117 "--ignore-decorators", 118 metavar="PATTERNS", 119 type=csv, 120 default=missing, 121 help=f"Comma-separated list of decorators. Functions and classes using" 122 f' these decorators are ignored (e.g., "@app.route,@require_*").' 123 f" {glob_help}", 124 ) 125 parser.add_argument( 126 "--ignore-names", 127 metavar="PATTERNS", 128 type=csv, 129 default=missing, 130 help=f'Comma-separated list of names to ignore (e.g., "visit_*,do_*").' 131 f" {glob_help}", 132 ) 133 parser.add_argument( 134 "--make-whitelist", 135 action="store_true", 136 default=missing, 137 help="Report unused code in a format that can be added to a" 138 " whitelist module.", 139 ) 140 parser.add_argument( 141 "--min-confidence", 142 type=int, 143 default=missing, 144 help="Minimum confidence (between 0 and 100) for code to be" 145 " reported as unused.", 146 ) 147 parser.add_argument( 148 "--sort-by-size", 149 action="store_true", 150 default=missing, 151 help="Sort unused functions and classes by their lines of code.", 152 ) 153 parser.add_argument( 154 "-v", "--verbose", action="store_true", default=missing 155 ) 156 parser.add_argument("--version", action="version", version=version) 157 namespace = parser.parse_args(args) 158 cli_args = { 159 key: value 160 for key, value in vars(namespace).items() 161 if value is not missing 162 } 163 _check_input_config(cli_args) 164 return cli_args 165 166 167def make_config(argv=None, tomlfile=None): 168 """ 169 Returns a config object for vulture, merging both ``pyproject.toml`` and 170 CLI arguments (CLI arguments have precedence). 171 172 :param argv: The CLI arguments to be parsed. This value is transparently 173 passed through to :py:meth:`argparse.ArgumentParser.parse_args`. 174 :param tomlfile: An IO instance containing TOML data. By default this will 175 auto-detect an existing ``pyproject.toml`` file and exists solely for 176 unit-testing. 177 """ 178 # If we loaded data from a TOML file, we want to print this out on stdout 179 # in verbose mode so we need to keep the value around. 180 detected_toml_path = "" 181 182 if tomlfile: 183 config = _parse_toml(tomlfile) 184 detected_toml_path = str(tomlfile) 185 else: 186 toml_path = pathlib.Path("pyproject.toml").resolve() 187 if toml_path.is_file(): 188 with open(toml_path) as fconfig: 189 config = _parse_toml(fconfig) 190 detected_toml_path = str(toml_path) 191 else: 192 config = {} 193 194 cli_config = _parse_args(argv) 195 196 # Overwrite TOML options with CLI options, if given. 197 config.update(cli_config) 198 199 # Set defaults for missing options. 200 for key, value in DEFAULTS.items(): 201 config.setdefault(key, value) 202 203 if detected_toml_path and config["verbose"]: 204 print(f"Reading configuration from {detected_toml_path}") 205 206 _check_output_config(config) 207 208 return config 209