-
-
Notifications
You must be signed in to change notification settings - Fork 638
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
Move option registration validation earlier #21645
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ | |
logger = logging.getLogger() | ||
|
||
|
||
def parse_dest(*args, **kwargs): | ||
def parse_dest(*args: str, **kwargs) -> str: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out this annotation means "all *args must be strings" to mypy, which is good. |
||
"""Return the dest for an option registration. | ||
|
||
If an explicit `dest` is specified, returns that and otherwise derives a default from the | ||
|
@@ -36,7 +36,7 @@ def parse_dest(*args, **kwargs): | |
""" | ||
dest = kwargs.get("dest") | ||
if dest: | ||
return dest | ||
return str(dest) | ||
# No explicit dest, so compute one based on the first long arg, or the short arg | ||
# if that's all there is. | ||
arg = next((a for a in args if a.startswith("--")), args[0]) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1014,7 +1014,7 @@ def test_parse_dest() -> None: | |
|
||
|
||
def test_validation() -> None: | ||
def assertError(expected_error, *args, **kwargs): | ||
def assert_error(expected_error, *args, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a rename for convention. |
||
with pytest.raises(expected_error): | ||
options = Options.create( | ||
args=["./pants"], | ||
|
@@ -1025,17 +1025,17 @@ def assertError(expected_error, *args, **kwargs): | |
options.register(GLOBAL_SCOPE, *args, **kwargs) | ||
options.for_global_scope() | ||
|
||
assertError(NoOptionNames) | ||
assertError(OptionNameDoubleDash, "badname") | ||
assertError(OptionNameDoubleDash, "-badname") | ||
assertError(InvalidKwarg, "--foo", badkwarg=42) | ||
assertError(BooleanOptionNameWithNo, "--no-foo", type=bool) | ||
assertError(MemberTypeNotAllowed, "--foo", member_type=int) | ||
assertError(MemberTypeNotAllowed, "--foo", type=dict, member_type=int) | ||
assertError(InvalidMemberType, "--foo", type=list, member_type=set) | ||
assertError(InvalidMemberType, "--foo", type=list, member_type=list) | ||
assertError(HelpType, "--foo", help=()) | ||
assertError(HelpType, "--foo", help=("Help!",)) | ||
assert_error(NoOptionNames) | ||
assert_error(OptionNameDoubleDash, "badname") | ||
assert_error(OptionNameDoubleDash, "-badname") | ||
assert_error(InvalidKwarg, "--foo", badkwarg=42) | ||
assert_error(BooleanOptionNameWithNo, "--no-foo", type=bool) | ||
assert_error(MemberTypeNotAllowed, "--foo", member_type=int) | ||
assert_error(MemberTypeNotAllowed, "--foo", type=dict, member_type=int) | ||
assert_error(InvalidMemberType, "--foo", type=list, member_type=set) | ||
assert_error(InvalidMemberType, "--foo", type=list, member_type=list) | ||
assert_error(HelpType, "--foo", help=()) | ||
assert_error(HelpType, "--foo", help=("Help!",)) | ||
|
||
|
||
def test_shadowing() -> None: | ||
|
@@ -1046,7 +1046,7 @@ def test_shadowing() -> None: | |
args=["./pants"], | ||
) | ||
options.register("", "--opt1") | ||
options.register("foo", "-o", "--opt2") | ||
options.register("foo", "--opt2") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a test that didn't adhere to our own validation rules, but skirted them since we don't parse in this test. |
||
|
||
|
||
def test_is_known_scope() -> None: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
from collections import defaultdict | ||
from dataclasses import dataclass | ||
from enum import Enum | ||
from typing import Any, Iterable, Mapping | ||
from typing import Any, Mapping | ||
|
||
from pants.base.deprecated import validate_deprecation_semver, warn_or_error | ||
from pants.option.custom_types import ( | ||
|
@@ -42,7 +42,7 @@ | |
from pants.option.native_options import NativeOptionParser, parse_dest | ||
from pants.option.option_value_container import OptionValueContainer, OptionValueContainerBuilder | ||
from pants.option.ranked_value import Rank, RankedValue | ||
from pants.option.scope import GLOBAL_SCOPE, ScopeInfo | ||
from pants.option.scope import GLOBAL_SCOPE | ||
from pants.util.strutil import softwrap | ||
|
||
logger = logging.getLogger(__name__) | ||
|
@@ -92,16 +92,12 @@ def _invert(cls, s: bool | str | None) -> bool | None: | |
b = cls.ensure_bool(s) | ||
return not b | ||
|
||
def __init__( | ||
self, | ||
scope_info: ScopeInfo, | ||
) -> None: | ||
def __init__(self, scope: str) -> None: | ||
"""Create a Parser instance. | ||
|
||
:param scope_info: the scope this parser acts for. | ||
""" | ||
self._scope_info = scope_info | ||
self._scope = self._scope_info.scope | ||
self._scope = scope | ||
|
||
# All option args registered with this parser. Used to prevent conflicts. | ||
self._known_args: set[str] = set() | ||
|
@@ -112,10 +108,6 @@ def __init__( | |
# Map of dest -> history. | ||
self._history: dict[str, OptionValueHistory] = {} | ||
|
||
@property | ||
def scope_info(self) -> ScopeInfo: | ||
return self._scope_info | ||
|
||
@property | ||
def scope(self) -> str: | ||
return self._scope | ||
|
@@ -125,38 +117,13 @@ def known_scoped_args(self) -> frozenset[str]: | |
prefix = f"{self.scope}-" if self.scope != GLOBAL_SCOPE else "" | ||
return frozenset(f"--{prefix}{arg.lstrip('--')}" for arg in self._known_args) | ||
|
||
def history(self, dest: str) -> OptionValueHistory | None: | ||
return self._history.get(dest) | ||
|
||
@dataclass(frozen=True) | ||
class ParseArgsRequest: | ||
namespace: OptionValueContainerBuilder | ||
passthrough_args: list[str] | ||
allow_unknown_flags: bool | ||
|
||
def __init__( | ||
self, | ||
flags_in_scope: Iterable[str], | ||
namespace: OptionValueContainerBuilder, | ||
passthrough_args: list[str], | ||
allow_unknown_flags: bool, | ||
) -> None: | ||
""" | ||
:param flags_in_scope: Iterable of arg strings to parse into flag values. | ||
:param namespace: The object to register the flag values on | ||
""" | ||
object.__setattr__(self, "namespace", namespace) | ||
object.__setattr__(self, "passthrough_args", passthrough_args) | ||
object.__setattr__(self, "allow_unknown_flags", allow_unknown_flags) | ||
|
||
def parse_args_native( | ||
self, | ||
native_parser: NativeOptionParser, | ||
) -> OptionValueContainer: | ||
namespace = OptionValueContainerBuilder() | ||
mutex_map = defaultdict(list) | ||
for args, kwargs in self._option_registrations: | ||
self._validate(args, kwargs) | ||
dest = parse_dest(*args, **kwargs) | ||
val, rank = native_parser.get_value( | ||
scope=self.scope, registration_args=args, registration_kwargs=kwargs | ||
|
@@ -213,9 +180,9 @@ def normalize_kwargs(orig_args, orig_kwargs): | |
|
||
def register(self, *args, **kwargs) -> None: | ||
"""Register an option.""" | ||
if args: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was needed because we were working with unvalidated args... 🤦 |
||
dest = parse_dest(*args, **kwargs) | ||
self._check_deprecated(dest, kwargs, print_warning=False) | ||
self._validate(args, kwargs) | ||
dest = parse_dest(*args, **kwargs) | ||
self._check_deprecated(dest, kwargs, print_warning=False) | ||
|
||
if self.is_bool(kwargs): | ||
default = kwargs.get("default") | ||
|
@@ -316,15 +283,19 @@ def error( | |
default_value = kwargs.get("default") | ||
if default_value is not None: | ||
if isinstance(default_value, str) and type_arg != str: | ||
# attempt to parse default value, for correctness.. | ||
# attempt to parse default value, for correctness. | ||
# custom function types may implement their own validation | ||
default_value = self.to_value_type(default_value, type_arg, member_type) | ||
if hasattr(default_value, "val"): | ||
default_value = default_value.val | ||
|
||
# fall through to type check, to verify that custom types returned a value of correct type | ||
|
||
if isinstance(type_arg, type) and not isinstance(default_value, type_arg): | ||
if ( | ||
isinstance(type_arg, type) | ||
and not isinstance(default_value, type_arg) | ||
and not (issubclass(type_arg, bool) and default_value == UnsetBool) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously validation tripped up on UnsetBool. I think the only reason this didn't blow up is that we don't actually use UnsetBool anywhere, and the tests were not validating. But now that they are, they caught this. Possibly we can get rid of UnsetBool, I don't see why None couldn't be used, but one thing at a time. |
||
): | ||
error( | ||
DefaultValueType, | ||
option_type=type_arg.__name__, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need "ScopeInfo" in some places now, and hopefully in all places soon...