-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Refactor and fix typing in coredata #11492
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
base: master
Are you sure you want to change the base?
Conversation
keywords = {'yielding': self.yielding, 'value': value} | ||
|
||
if T.TYPE_CHECKING: | ||
class KeywordDict(T.TypedDict, total=False): |
Check notice
Code scanning / CodeQL
Unused local variable
return self.default | ||
|
||
def add_to_argparse(self, name: str, parser: argparse.ArgumentParser, help_suffix: str) -> None: | ||
kwargs = OrderedDict() | ||
if T.TYPE_CHECKING: | ||
class ArgumentKW(T.TypedDict, total=False): |
Check notice
Code scanning / CodeQL
Unused local variable
Codecov Report
@@ Coverage Diff @@
## master #11492 +/- ##
==========================================
- Coverage 71.24% 68.83% -2.41%
==========================================
Files 212 209 -3
Lines 46390 45583 -807
Branches 11021 9439 -1582
==========================================
- Hits 33049 31377 -1672
- Misses 10881 11782 +901
+ Partials 2460 2424 -36 |
coredata: fix a remaining type issues Do you mean |
Then if you're really nitpicky, one of the commits is |
96d8f14
to
2e721cc
Compare
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.
last commit message: "inisde" should be "inside"
mesonbuild/coredata.py
Outdated
|
||
def __post_init__(self) -> None: | ||
# This should be possible with an `InitVar`, but it doesn't seem to work | ||
# with mypy 0.991. |
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.
What's the problem? Is it fixed with mypy 1.0?
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.
if you have:
@dataclass
class C(Generic[_T]):
value: InitVar[_T]
def __post_init__(self, value: _T):
self.value = value
It becomes convinced that C
doesn't have an attribute value
. I figured out I can work around this with value_: InitVar[_T]
, so I'll do that.
mesonbuild/coredata.py
Outdated
super().__init__(description, [True, False], yielding, deprecated) | ||
self.set_value(value) | ||
|
||
choices: T.List[bool] = field(default_factory=lambda: [True, False]) |
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.
Do we need a factory field here (and elsewhere)? AFAICT we never modify choices in place, only assign them as = newthing
, and never for boolean or umask anyway. Can typing_extensions.Final
help here?
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.
Particularly given this is a static choice on the option type, I feel like this should be a ClassVar, but apparently you cannot mix ClassVar and Final: https://bugs.python.org/issue45384
Pity. But personally I'd still go with a ClassVar I guess.
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.
This is all really annoying to solve because with dataclasses you can't forward declare an attribute. I've done my best here by adding an invisible attribute for choices
, and then overriding it all of the subclasses. The problem is that without it being defined in UserOption, UserOption can't be used as a generic interface, and mypy says "hey, that dict of [str: UserOption] doesn't have .choices!"
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.
Actually, I realized I'm wrong, you can forward declare them, you just have to do it the old-school way.
@dataclass
class C:
x: int
if typing.TYPE_CHECKING:
y: str
But it doesn't help us in this case
mesonbuild/coredata.py
Outdated
@@ -190,27 +191,34 @@ def printable_value(self) -> str: | |||
def validate_value(self, value: T.Any) -> T.Union[str, OctalInt]: | |||
if value is None or value == 'preserve': | |||
return 'preserve' | |||
return OctalInt(super().validate_value(value)) | |||
if isinstance(value, str): |
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.
I haven't investigated for myself, but what's the reason we can't inherit from integer options anymore and have to copy this over?
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.
Because in that case we end up with conflicting types for _T
, UserIntegerOption
requires that _T is int
while UserOption[str | OctalInt]
requires _T in [str, OctalInt]
, so you end up with things like validate_value
must simultaneously return an int
and str | OctalInt
, which is obviously impossible.
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.
Yay... and I guess using a mixin or UserIntegerOption.validate_value(self, value)
feel too hacky or something?
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.
I didn't think about using a mixin... that would work
2e721cc
to
7506f19
Compare
I haven't fixed everything, but I've tried to fix the choices. It's a bit of a challenge because of data classes though. I think the most elegant fix would require dropping them |
288b9d8
to
ef878a8
Compare
mesonbuild/coredata.py
Outdated
toint: T.Callable[[str], int] | ||
|
||
# This has to be done without super() because the two consumers have | ||
# differen types, and mypy gets grumpy in UserUmaskOption that |
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.
# differen types, and mypy gets grumpy in UserUmaskOption that | |
# different types, and mypy gets grumpy in UserUmaskOption that |
mesonbuild/coredata.py
Outdated
|
||
def __post_init__(self, value: str) -> None: | ||
if not self.choices or not all(isinstance(c, str) for c in self.choices): | ||
raise MesonException('Combo option must have choices, which must be a list, and must not be empty') |
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.
a list
The previous code was more meson-style, which is to say it referred to an array.
|
||
choices: T.List[str] = field(default_factory=list) | ||
split_args: bool = False | ||
user_input: bool = False |
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.
I think we only need user_input here to pass it to validate_value during post_init, which we currently don't do because we rely on the post_init from UserOption.
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.
I think I have fixed this on the next push
mesonbuild/optinterpreter.py
Outdated
@@ -266,10 +265,12 @@ def integer_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS] | |||
def string_array_parser(self, description: str, args: T.Tuple[bool, _DEPRECATED_ARGS], kwargs: StringArrayArgs) -> coredata.UserOption: | |||
choices = kwargs['choices'] | |||
value = kwargs['value'] if kwargs['value'] is not None else choices | |||
if isinstance(value, str): | |||
value = [value] |
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.
Should this be a convertor for the KwargInfo?
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 can still hit this from -Darray-opt='foo'
I think
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.
I tested this code path with meson setup -Darray=foo,bar
with an assert on the first line of the function. Was not hit. Not sure how this function is called in practice.
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.
I am an idiot. This code path is no longer hit. -Darray-opt=item
is a list by the time we are here.
mesonbuild/coredata.py
Outdated
def printable_value(self) -> str: | ||
if self.value == 'preserve': | ||
def printable_value(self) -> StringProtocol: | ||
if isinstance(self.value, str): | ||
return self.value | ||
return format(self.value, '04o') |
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.
Is mypy not clever enough here to notice that in all cases of a non-str, it falls through to format()?
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.
yeah, because we haven't actually annotated value to be Literal['presever'] | int
. Maybe we can try that now, actually...
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.
I tried this, but ended up breaking mypy somehow. Needs more investigating...
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.
This is kinda challenging, because we expect to be able to pass in string values representing octals like '022'
in install_umask
. I don't think we can have our cake and eat it too.
mesonbuild/coredata.py
Outdated
@@ -668,15 +680,16 @@ def get_option(self, key: OptionKey) -> T.Union[T.List[str], str, int, bool, Wra | |||
if v.yielding: | |||
if key.name == 'wrap_mode': | |||
return WrapMode[v.value] | |||
return v.value | |||
return T.cast(T.Union[T.List[str], str, int, bool], v.value) |
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.
... and here.
mesonbuild/coredata.py
Outdated
def get_nondefault_buildtype_args(self): | ||
result = [] | ||
def get_nondefault_buildtype_args(self) -> T.List[T.Union[T.Tuple[Literal['optimization'], str, str], T.Tuple[Literal['debug'], bool, bool]]]: | ||
result: T.List[T.Union[T.Tuple[Literal['optimization'], str, str], T.Tuple[Literal['debug'], bool, bool]]] = [] |
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.
This is long enough and difficult enough to read, just because of the annotation, that I would like to see it defined as a readable type in T.TYPE_CHECKING and then reused here, even if it was only used once. :D But lucky us, we can use it twice.
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.
Done, but feel free to bikeshed the name
@@ -783,15 +803,15 @@ def _set_others_from_buildtype(self, value: str) -> None: | |||
|
|||
@staticmethod | |||
def is_per_machine_option(optname: OptionKey) -> bool: | |||
if optname.name in BUILTIN_OPTIONS_PER_MACHINE: | |||
if optname in BUILTIN_OPTIONS_PER_MACHINE: |
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.
Okay, so this is buggy... but what did it do before? The function is used twice, once in mintro (I do not care what that does), and once in environment.py -- in the latter case, it is being used to "Keep only per machine options from the native file" Not sure I understand the ramifications of changing this.
mesonbuild/coredata.py
Outdated
return True | ||
return optname.lang is not None | ||
|
||
def get_external_args(self, for_machine: MachineChoice, lang: str) -> T.List[str]: | ||
return self.options[OptionKey('args', machine=for_machine, lang=lang)].value | ||
return T.cast(T.List[str], self.options[OptionKey('args', machine=for_machine, lang=lang)].value) |
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.
String-quote first argument to T.cast.
mesonbuild/coredata.py
Outdated
@@ -1193,13 +1230,21 @@ def argparse_name_to_arg(name: str) -> str: | |||
def prefixed_default(self, name: 'OptionKey', prefix: str = '') -> _T: | |||
if self.opt_type is UserStringOption: | |||
try: | |||
return BULITIN_DIR_NOPREFIX_OPTIONS[name][prefix] | |||
# _T is str in this case, but mypy can't figure that out | |||
return T.cast(_T, BULITIN_DIR_NOPREFIX_OPTIONS[name][prefix]) |
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.
T.cast first argument.
This does a ton of cleanup to the UserOption classes. They are converted into dataclasses, and much of their constructor logic is removed, and what is left is simplified. This also cleans up their typing considerably
This is again, simplified, and the typing has been fixed
For one, they have a BuiltinOption not a UserOption, and for two, they shouldn't be mutable (at least the dicts themselves shouldn't)
There are a lot of them in here. Some of them mypy is spotting legitimate Generic deviations but that is in part because we can't tell mypy what we really mean: listify for example should always return a `List[T where T is scalar]`, so but the strict generics of an array option would be `List[List[T]]`, so we just have to sprinkle in some `type: ignore` comments
ef878a8
to
cb48934
Compare
I did a rebase, and @tristan957 helped with some of the review comments. Thanks Tristan! |
The typing (and some of the inheritance model) of the UserOptions was just wrong (I didn't handle generics right the first time). I've simplified both the UserOption and BuiltinOption classes, converted them to dataclasses (yay dataclasses!) and then cleaned up the remaining typing issues. There is some churn because I normalized the constructors for the UserOptions, which changed the order of ComboOptions to match that of the other Option types.