-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Make the new options module fully type safe #13620
base: master
Are you sure you want to change the base?
Make the new options module fully type safe #13620
Conversation
2fbff61
to
8881a19
Compare
self.__choices: str = ', '.join(choices) | ||
|
||
def printable_choices(self) -> T.Optional[T.List[str]]: | ||
return [self.__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.
Overloading printable_choices()
to return a description of the valid range is a bit weird. The use of self.__choices
makes it even less understandable. Would it make sense to move the computation of the string returned by printable_choices()
into the method itself? I don't think that this code path is performance sensitive and requires caching the computation, but if it does, decorating the method with @functools.cache
may be a better solution.
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 super important actually, because choices needs to be a Container[T]
, matching the type of the UserOption[T]
, but printable choices needs to be List[str] | None
, regardless of what T
is. The entire point of printable_choices
is to fix the fact that we both need a string definable set of choices (like those used in Combo and Array options that are essentially a set of valid values, and the choices of integer options, where the constraints are a range of valid values. The status quo is that IntegerOption
shoves '>=0, < 100'
into choices
and acts like it's cool, but then BooleanOption
puts [True, False]
in there.
Specifically, printable_choices gets used for printing the configuration and in the introspection code.
8881a19
to
00ae5b5
Compare
@dnicolodi: I've added an explicit method for checking the choices, so all of that is hidden inside the options module, which I think is a bit more elegant than having coredata poke at the internals of the |
00ae5b5
to
58437fe
Compare
The suggestion to add the |
""" | ||
if isinstance(a, EnumeratedUserOption): | ||
# If this isn't hte case the above failed somehow | ||
assert isinstance(b, EnumeratedUserOption), 'for mypy' |
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 really this dumb? Isn't isinstance()
recognized as a type narrowing check? I don't know what else is then...
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.
It is a type narrowing check, but only for a
, b
is still left as _U
, and even when I tried adding things like:
assert type(a) is type(b)
mypy wasn't smart enough to figure out that if a
is EnumeratedUserOption
that b
would also be EnumeratedUserOption
.
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.
Oh, sorry, I misunderstood how type vars work when they reference unions. I though that if the same type vars appears in a function signature it is forced to resolve to the same concrete type. Evidently it does not work like this.
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.
Maybe it should? It would in rust and C++ wouldn't it? Maybe I should open a typing issue about this.
58437fe
to
6b288f8
Compare
@dnicolodi fixed the typo, and dropped the range patch. I played around with getting better error messages, and just felt like it wasn't worth the amount of code involved. |
6b288f8
to
60c9113
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.
This isn't directly related to this PR, but there is a lot of repetition for defining options, especially for the winlibs
option. I wonder if a global dict or function should be used to prevent having a different description depending where the option is defined?
60c9113
to
0539929
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.
Overall, looks good!
key = self.form_compileropt_key('eh') | ||
opts[key] = options.UserComboOption( | ||
self.make_option_name(key), | ||
'C++ exception handling type.', |
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.
nitpick: this one ends with a .
, while other options don't...
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.
That has apparently always existed. The main branch right now has that peroid
|
||
def init_option(self, name: 'OptionKey', value: T.Optional[T.Any], prefix: str) -> UserOption[_T]: | ||
"""Create an instance of opt_type and return it.""" | ||
if value is None: | ||
value = self.prefixed_default(name, prefix) | ||
keywords = {'yielding': self.yielding, 'value': value} | ||
keywords = {'yielding': self.yielding, '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.
Where does that value_
comes from?
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 to use dataclasses I had to rename the argument from value
to value_
, otherwise you can't convert the type. See python/mypy#15423, where I argued this was a bug but upstream argued it was the intended behavior.
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.
Ah, but python doesn't need convertors for dataclasses, do we...
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'd laugh if the whole thing didn't make me so mad
0539929
to
9f0e17d
Compare
I cherry-picked a few (more) of these to master. Haven't taken a close look at the rest and it's getting late on Friday. |
9f0e17d
to
8c97002
Compare
This provides a method to get choices for options in a printable form. The goal is to make refactoring options simpler.
This saves a *tiny* bit of typing, but at the cost of requiring either the current solution of throwing up our hands and saying "typing is too hard, better to have bugs!" or an extensive amount of `TypedDict`s, `overloads`, and a very new version of mypy. Let's get our type safety back, even if it means writing a little bit more code.
This reduces code, makes this clearer, and will be a nice step toward the goal of getting everything typesafe
This will allow us to take choices out of the UserOption class, which doesn't actually use this attribute.
Which wants a string, but then passes that string to a function that wants an OptionKey, which means that we'll always miss the lookup in BULITIN_DIR_NOPREFIX_OPTIONS, and return the default. The only case this gets used we cast an OptionKey to str, and then pass that. So instead, do the cast inside the function when necessary and pass the OptionKey
They are very similar, but they are not exactly the same. By splitting them we can get full type safety, and run mypy over the options.py file!
This allows us to hide type differences inside the options module, but still get accurate change information.
…on types The fact that UserOption is generic is really an implementation detail, not something to be used publicly. So by having an `AnyOptionType` alias, we can get better type checking, as can be seen by the patch as a whole. One of the big fixes it replace open-coded equivlalents of `MutableKeydOptionDictType` with that type alias.
8c97002
to
4a69ef0
Compare
mesonbuild/compilers/compilers.py
Outdated
_T = T.TypeVar('_T') | ||
UserOptionType = T.TypeVar('UserOptionType', bound=options.UserOption) | ||
|
||
_T = T.TypeVar('_T') | ||
|
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 diff hunk confused me because it seems to be in the wrong commit.
@@ -63,7 +63,8 @@ def get_options(self) -> coredata.MutableKeyedOptionDictType: | |||
opts[key] = options.UserIntegerOption( | |||
self.make_option_name(key), | |||
'Number of threads to use in web assembly, set to 0 to disable', | |||
(0, None, 4)) # Default was picked at random | |||
4, # Default was picked at random | |||
min_value=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.
I think this may be a good case where it's worth explaining the change in the commit message. After thinking about this, I think that this works because the dataclass also fits a historic mess where the "value" member isn't consistently used by all option types, when we do stuff like overloading it with minimum/maximum values instead?
This fixes all of the issues in the options module, as well as fixing type safety issues introduced by it. This really falls into four parts:
UserOption
classesAny
that would hide real issuesOptionStore
class.This fully supersedes #13616