Skip to content

Commit 5b5fd2b

Browse files
create command-line options for each config option (#373)
1 parent b954348 commit 5b5fd2b

File tree

7 files changed

+163
-2
lines changed

7 files changed

+163
-2
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Create command-line options for each config option (#373)
56
- Overhaul treatment of function definitions (#372)
67
- Support positional-only arguments
78
- Infer more precise types for lambda functions

docs/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,15 @@ Other supported configuration options are listed below.
2929

3030
Almost all configuration options can be overridden for individual modules or packages. To set a module-specific configuration, add an entry to the `tool.pyanalyze.overrides` list (as in the example above), and set the `module` key to the fully qualified name of the module or package.
3131

32+
To see the current value of all configuration options, pass the `--display-options` command-line option:
33+
34+
```
35+
$ python -m pyanalyze --config-file pyproject.toml --display-options
36+
Options:
37+
add_import (value: True)
38+
...
39+
```
40+
41+
Most configuration options can also be set on the command line.
42+
3243
<!-- TODO figure out a way to dynamically include docs for each option -->

pyanalyze/name_check_visitor.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
IntegerOption,
7777
Options,
7878
StringSequenceOption,
79+
add_arguments,
7980
)
8081
from .shared_options import Paths, ImportPaths, EnforceNoUnused
8182
from .reexport import ImplicitReexportTracker
@@ -399,6 +400,9 @@ class IgnoredPaths(ConcatenatedOption[Sequence[str]]):
399400
name = "ignored_paths"
400401
default_value = ()
401402

403+
# too complicated and this option isn't too useful anyway
404+
should_create_command_line_option = False
405+
402406
@classmethod
403407
def get_value_from_fallback(cls, fallback: Config) -> Sequence[Sequence[str]]:
404408
return fallback.IGNORED_PATHS
@@ -4437,6 +4441,13 @@ def _get_argument_parser(cls) -> ArgumentParser:
44374441
type=Path,
44384442
help="Path to a pyproject.toml configuration file",
44394443
)
4444+
parser.add_argument(
4445+
"--display-options",
4446+
action="store_true",
4447+
default=False,
4448+
help="Display the options used for this check, then exit",
4449+
)
4450+
add_arguments(parser)
44404451
return parser
44414452

44424453
@classmethod
@@ -4477,15 +4488,26 @@ def prepare_constructor_kwargs(cls, kwargs: Mapping[str, Any]) -> Mapping[str, A
44774488
for error_code, value in kwargs["settings"].items():
44784489
option_cls = ConfigOption.registry[error_code.name]
44794490
instances.append(option_cls(value, from_command_line=True))
4480-
if "files" in kwargs:
4481-
instances.append(Paths(kwargs.pop("files"), from_command_line=True))
4491+
files = kwargs.pop("files", [])
4492+
if files:
4493+
instances.append(Paths(files, from_command_line=True))
4494+
for name, option_cls in ConfigOption.registry.items():
4495+
if not option_cls.should_create_command_line_option:
4496+
continue
4497+
if name not in kwargs:
4498+
continue
4499+
value = kwargs.pop(name)
4500+
instances.append(option_cls(value, from_command_line=True))
44824501
config_file = kwargs.pop("config_file", None)
44834502
if config_file is None:
44844503
config_filename = cls.config_filename
44854504
if config_filename is not None:
44864505
module_path = Path(sys.modules[cls.__module__].__file__).parent
44874506
config_file = module_path / config_filename
44884507
options = Options.from_option_list(instances, cls.config, config_file)
4508+
if kwargs.pop("display_options", False):
4509+
options.display()
4510+
sys.exit(0)
44894511
kwargs.setdefault("checker", Checker(cls.config, options))
44904512
return kwargs
44914513

pyanalyze/options.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
Structured configuration options.
44
55
"""
6+
import argparse
67
from collections import defaultdict
78
from dataclasses import dataclass
89
from pathlib import Path
10+
import pathlib
911
from typing import (
1012
Any,
1113
ClassVar,
@@ -27,6 +29,37 @@
2729
from .error_code import ErrorCode
2830
from .safe import safe_in
2931

32+
try:
33+
from argparse import BooleanOptionalAction
34+
except ImportError:
35+
# 3.8 and lower (modified from CPython)
36+
class BooleanOptionalAction(argparse.Action):
37+
def __init__(self, option_strings: Sequence[str], **kwargs: Any) -> None:
38+
39+
_option_strings = []
40+
for option_string in option_strings:
41+
_option_strings.append(option_string)
42+
43+
if option_string.startswith("--"):
44+
option_string = "--no-" + option_string[2:]
45+
_option_strings.append(option_string)
46+
47+
super().__init__(option_strings=_option_strings, nargs=0, **kwargs)
48+
49+
def __call__(
50+
self,
51+
parser: argparse.ArgumentParser,
52+
namespace: argparse.Namespace,
53+
values: object,
54+
option_string: Optional[str] = None,
55+
) -> None:
56+
if option_string is not None and option_string in self.option_strings:
57+
setattr(namespace, self.dest, not option_string.startswith("--no-"))
58+
59+
def format_usage(self) -> str:
60+
return " | ".join(self.option_strings)
61+
62+
3063
T = TypeVar("T")
3164
OptionT = TypeVar("OptionT", bound="ConfigOption")
3265
ModulePath = Tuple[str, ...]
@@ -56,6 +89,7 @@ class ConfigOption(Generic[T]):
5689
name: ClassVar[str]
5790
is_global: ClassVar[bool] = False
5891
default_value: ClassVar[T]
92+
should_create_command_line_option: ClassVar[bool] = True
5993
value: T
6094
applicable_to: ModulePath = ()
6195
from_command_line: bool = False
@@ -104,6 +138,10 @@ def get_fallback_option(cls: Type[OptionT], fallback: Config) -> Optional[Option
104138
else:
105139
return cls(val)
106140

141+
@classmethod
142+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
143+
raise NotImplementedError(cls)
144+
107145

108146
class BooleanOption(ConfigOption[bool]):
109147
default_value = False
@@ -114,6 +152,15 @@ def parse(cls: "Type[BooleanOption]", data: object, source_path: Path) -> bool:
114152
return data
115153
raise InvalidConfigOption.from_parser(cls, "bool", data)
116154

155+
@classmethod
156+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
157+
parser.add_argument(
158+
f"--{cls.name.replace('_', '-')}",
159+
action=BooleanOptionalAction,
160+
help=cls.__doc__,
161+
default=argparse.SUPPRESS,
162+
)
163+
117164

118165
class IntegerOption(ConfigOption[int]):
119166
@classmethod
@@ -122,6 +169,15 @@ def parse(cls: "Type[IntegerOption]", data: object, source_path: Path) -> int:
122169
return data
123170
raise InvalidConfigOption.from_parser(cls, "int", data)
124171

172+
@classmethod
173+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
174+
parser.add_argument(
175+
f"--{cls.name.replace('_', '-')}",
176+
type=int,
177+
help=cls.__doc__,
178+
default=argparse.SUPPRESS,
179+
)
180+
125181

126182
class ConcatenatedOption(ConfigOption[Sequence[T]]):
127183
"""Option for which the value is the concatenation of all the overrides."""
@@ -152,6 +208,15 @@ def parse(
152208
return data
153209
raise InvalidConfigOption.from_parser(cls, "sequence of strings", data)
154210

211+
@classmethod
212+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
213+
parser.add_argument(
214+
f"--{cls.name.replace('_', '-')}",
215+
action="append",
216+
help=cls.__doc__,
217+
default=argparse.SUPPRESS,
218+
)
219+
155220

156221
class PathSequenceOption(ConfigOption[Sequence[Path]]):
157222
default_value: ClassVar[Sequence[Path]] = ()
@@ -166,6 +231,16 @@ def parse(
166231
return [(source_path.parent / elt).resolve() for elt in data]
167232
raise InvalidConfigOption.from_parser(cls, "sequence of strings", data)
168233

234+
@classmethod
235+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
236+
parser.add_argument(
237+
f"--{cls.name.replace('_', '-')}",
238+
action="append",
239+
type=pathlib.Path,
240+
help=cls.__doc__,
241+
default=argparse.SUPPRESS,
242+
)
243+
169244

170245
class PyObjectSequenceOption(ConfigOption[Sequence[T]]):
171246
"""Represents a sequence of objects parsed as Python objects."""
@@ -197,6 +272,16 @@ def contains(cls, obj: object, options: "Options") -> bool:
197272
val = options.get_value_for(cls)
198273
return safe_in(obj, val)
199274

275+
@classmethod
276+
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
277+
parser.add_argument(
278+
f"--{cls.name.replace('_', '-')}",
279+
action="append",
280+
type=qcore.object_from_string,
281+
help=cls.__doc__,
282+
default=argparse.SUPPRESS,
283+
)
284+
200285

201286
@dataclass
202287
class Options:
@@ -262,6 +347,34 @@ def is_error_code_enabled_anywhere(self, code: ErrorCode) -> bool:
262347
return False
263348
return option.default_value
264349

350+
def display(self) -> None:
351+
print("Options:")
352+
prefix = " " * 8
353+
for name, option_cls in sorted(ConfigOption.registry.items()):
354+
current_value = self.get_value_for(option_cls)
355+
print(f" {name} (value: {current_value})")
356+
instances = self.options.get(name, [])
357+
for instance in instances:
358+
pieces = []
359+
if instance.applicable_to:
360+
pieces.append(f"module: {'.'.join(instance.applicable_to)}")
361+
if instance.from_command_line:
362+
pieces.append("from command line")
363+
else:
364+
pieces.append("from config file")
365+
suffix = f" ({', '.join(pieces)})"
366+
print(f"{prefix}{instance.value}{suffix}")
367+
print(f"Fallback: {self.fallback}")
368+
if self.module_path:
369+
print(f"For module: {'.'.join(self.module_path)}")
370+
371+
372+
def add_arguments(parser: argparse.ArgumentParser) -> None:
373+
for cls in ConfigOption.registry.values():
374+
if not cls.should_create_command_line_option:
375+
continue
376+
cls.create_command_line_option(parser)
377+
265378

266379
def parse_config_file(path: Path) -> Iterable[ConfigOption]:
267380
with path.open("rb") as f:

pyanalyze/shared_options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Paths(PathSequenceOption):
1818

1919
name = "paths"
2020
is_global = True
21+
should_create_command_line_option = False
2122

2223
@classmethod
2324
def get_value_from_fallback(cls, fallback: Config) -> Sequence[Path]:
@@ -62,5 +63,6 @@ def get_value_from_fallback(cls, fallback: Config) -> Sequence[VariableNameValue
6263
"__doc__": ERROR_DESCRIPTION[_code],
6364
"name": _code.name,
6465
"default_value": _code not in DISABLED_BY_DEFAULT,
66+
"should_create_command_line_option": False,
6567
},
6668
)

pyanalyze/stacked_scopes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ def get_varname(self) -> Varname:
153153
)
154154
return self.varname
155155

156+
def __str__(self) -> str:
157+
pieces = [self.varname]
158+
for index, _ in self.indices:
159+
if isinstance(index, str):
160+
pieces.append(f".{index}")
161+
else:
162+
pieces.append(f"[{index.val!r}]")
163+
return "".join(pieces)
164+
156165

157166
SubScope = Dict[Varname, List[Node]]
158167

pyanalyze/value.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,6 +1699,9 @@ class ConstraintExtension(Extension):
16991699
def __hash__(self) -> int:
17001700
return id(self)
17011701

1702+
def __str__(self) -> str:
1703+
return str(self.constraint)
1704+
17021705

17031706
@dataclass(frozen=True, eq=False)
17041707
class NoReturnConstraintExtension(Extension):

0 commit comments

Comments
 (0)