Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create command-line options for each config option #373

Merged
merged 4 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Create command-line options for each config option (#373)
- Overhaul treatment of function definitions (#372)
- Support positional-only arguments
- Infer more precise types for lambda functions
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ Other supported configuration options are listed below.

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.

To see the current value of all configuration options, pass the `--display-options` command-line option:

```
$ python -m pyanalyze --config-file pyproject.toml --display-options
Options:
add_import (value: True)
...
```

Most configuration options can also be set on the command line.

<!-- TODO figure out a way to dynamically include docs for each option -->
26 changes: 24 additions & 2 deletions pyanalyze/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
IntegerOption,
Options,
StringSequenceOption,
add_arguments,
)
from .shared_options import Paths, ImportPaths, EnforceNoUnused
from .reexport import ImplicitReexportTracker
Expand Down Expand Up @@ -399,6 +400,9 @@ class IgnoredPaths(ConcatenatedOption[Sequence[str]]):
name = "ignored_paths"
default_value = ()

# too complicated and this option isn't too useful anyway
should_create_command_line_option = False

@classmethod
def get_value_from_fallback(cls, fallback: Config) -> Sequence[Sequence[str]]:
return fallback.IGNORED_PATHS
Expand Down Expand Up @@ -4437,6 +4441,13 @@ def _get_argument_parser(cls) -> ArgumentParser:
type=Path,
help="Path to a pyproject.toml configuration file",
)
parser.add_argument(
"--display-options",
action="store_true",
default=False,
help="Display the options used for this check, then exit",
)
add_arguments(parser)
return parser

@classmethod
Expand Down Expand Up @@ -4477,15 +4488,26 @@ def prepare_constructor_kwargs(cls, kwargs: Mapping[str, Any]) -> Mapping[str, A
for error_code, value in kwargs["settings"].items():
option_cls = ConfigOption.registry[error_code.name]
instances.append(option_cls(value, from_command_line=True))
if "files" in kwargs:
instances.append(Paths(kwargs.pop("files"), from_command_line=True))
files = kwargs.pop("files", [])
if files:
instances.append(Paths(files, from_command_line=True))
for name, option_cls in ConfigOption.registry.items():
if not option_cls.should_create_command_line_option:
continue
if name not in kwargs:
continue
value = kwargs.pop(name)
instances.append(option_cls(value, from_command_line=True))
config_file = kwargs.pop("config_file", None)
if config_file is None:
config_filename = cls.config_filename
if config_filename is not None:
module_path = Path(sys.modules[cls.__module__].__file__).parent
config_file = module_path / config_filename
options = Options.from_option_list(instances, cls.config, config_file)
if kwargs.pop("display_options", False):
options.display()
sys.exit(0)
kwargs.setdefault("checker", Checker(cls.config, options))
return kwargs

Expand Down
113 changes: 113 additions & 0 deletions pyanalyze/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
Structured configuration options.

"""
import argparse
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
import pathlib
from typing import (
Any,
ClassVar,
Expand All @@ -27,6 +29,37 @@
from .error_code import ErrorCode
from .safe import safe_in

try:
from argparse import BooleanOptionalAction
except ImportError:
# 3.8 and lower (modified from CPython)
class BooleanOptionalAction(argparse.Action):
def __init__(self, option_strings: Sequence[str], **kwargs: Any) -> None:

_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)

if option_string.startswith("--"):
option_string = "--no-" + option_string[2:]
_option_strings.append(option_string)

super().__init__(option_strings=_option_strings, nargs=0, **kwargs)

def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: object,
option_string: Optional[str] = None,
) -> None:
if option_string is not None and option_string in self.option_strings:
setattr(namespace, self.dest, not option_string.startswith("--no-"))

def format_usage(self) -> str:
return " | ".join(self.option_strings)


T = TypeVar("T")
OptionT = TypeVar("OptionT", bound="ConfigOption")
ModulePath = Tuple[str, ...]
Expand Down Expand Up @@ -56,6 +89,7 @@ class ConfigOption(Generic[T]):
name: ClassVar[str]
is_global: ClassVar[bool] = False
default_value: ClassVar[T]
should_create_command_line_option: ClassVar[bool] = True
value: T
applicable_to: ModulePath = ()
from_command_line: bool = False
Expand Down Expand Up @@ -104,6 +138,10 @@ def get_fallback_option(cls: Type[OptionT], fallback: Config) -> Optional[Option
else:
return cls(val)

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
raise NotImplementedError(cls)


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

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
f"--{cls.name.replace('_', '-')}",
action=BooleanOptionalAction,
help=cls.__doc__,
default=argparse.SUPPRESS,
)


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

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
f"--{cls.name.replace('_', '-')}",
type=int,
help=cls.__doc__,
default=argparse.SUPPRESS,
)


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

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
f"--{cls.name.replace('_', '-')}",
action="append",
help=cls.__doc__,
default=argparse.SUPPRESS,
)


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

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
f"--{cls.name.replace('_', '-')}",
action="append",
type=pathlib.Path,
help=cls.__doc__,
default=argparse.SUPPRESS,
)


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

@classmethod
def create_command_line_option(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
f"--{cls.name.replace('_', '-')}",
action="append",
type=qcore.object_from_string,
help=cls.__doc__,
default=argparse.SUPPRESS,
)


@dataclass
class Options:
Expand Down Expand Up @@ -262,6 +347,34 @@ def is_error_code_enabled_anywhere(self, code: ErrorCode) -> bool:
return False
return option.default_value

def display(self) -> None:
print("Options:")
prefix = " " * 8
for name, option_cls in sorted(ConfigOption.registry.items()):
current_value = self.get_value_for(option_cls)
print(f" {name} (value: {current_value})")
instances = self.options.get(name, [])
for instance in instances:
pieces = []
if instance.applicable_to:
pieces.append(f"module: {'.'.join(instance.applicable_to)}")
if instance.from_command_line:
pieces.append("from command line")
else:
pieces.append("from config file")
suffix = f" ({', '.join(pieces)})"
print(f"{prefix}{instance.value}{suffix}")
print(f"Fallback: {self.fallback}")
if self.module_path:
print(f"For module: {'.'.join(self.module_path)}")


def add_arguments(parser: argparse.ArgumentParser) -> None:
for cls in ConfigOption.registry.values():
if not cls.should_create_command_line_option:
continue
cls.create_command_line_option(parser)


def parse_config_file(path: Path) -> Iterable[ConfigOption]:
with path.open("rb") as f:
Expand Down
2 changes: 2 additions & 0 deletions pyanalyze/shared_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Paths(PathSequenceOption):

name = "paths"
is_global = True
should_create_command_line_option = False

@classmethod
def get_value_from_fallback(cls, fallback: Config) -> Sequence[Path]:
Expand Down Expand Up @@ -62,5 +63,6 @@ def get_value_from_fallback(cls, fallback: Config) -> Sequence[VariableNameValue
"__doc__": ERROR_DESCRIPTION[_code],
"name": _code.name,
"default_value": _code not in DISABLED_BY_DEFAULT,
"should_create_command_line_option": False,
},
)
9 changes: 9 additions & 0 deletions pyanalyze/stacked_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ def get_varname(self) -> Varname:
)
return self.varname

def __str__(self) -> str:
pieces = [self.varname]
for index, _ in self.indices:
if isinstance(index, str):
pieces.append(f".{index}")
else:
pieces.append(f"[{index.val!r}]")
return "".join(pieces)


SubScope = Dict[Varname, List[Node]]

Expand Down
3 changes: 3 additions & 0 deletions pyanalyze/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,9 @@ class ConstraintExtension(Extension):
def __hash__(self) -> int:
return id(self)

def __str__(self) -> str:
return str(self.constraint)


@dataclass(frozen=True, eq=False)
class NoReturnConstraintExtension(Extension):
Expand Down