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

Make the new options module fully type safe #13620

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

dcbaker
Copy link
Member

@dcbaker dcbaker commented Aug 30, 2024

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:

  1. fix the pre-existing issues with the UserOption classes
  2. clean up some uses of Any that would hide real issues
  3. Correct annotations for the OptionKey type that were broken
  4. add missing annotations to the OptionStore class.

This fully supersedes #13616

@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch 2 times, most recently from 2fbff61 to 8881a19 Compare August 30, 2024 22:05
self.__choices: str = ', '.join(choices)

def printable_choices(self) -> T.Optional[T.List[str]]:
return [self.__choices]
Copy link
Member

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.

Copy link
Member Author

@dcbaker dcbaker Sep 3, 2024

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.

mesonbuild/coredata.py Outdated Show resolved Hide resolved
@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 8881a19 to 00ae5b5 Compare September 3, 2024 16:52
@dcbaker
Copy link
Member Author

dcbaker commented Sep 3, 2024

@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 UserOption classes. I've also added a Range[T] implementation. I'm not sure on it, it does make some things nicer, but its more code and makes other things more complex, so for now I haven't folded into the series itself, but left it separate so it can be evaluated separately.

@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 00ae5b5 to 58437fe Compare September 3, 2024 18:07
@dnicolodi
Copy link
Member

The suggestion to add the Range type was done in the optic of allowing to compare the choice member of enumerated options types and int option types transparently, thus it is somehow alternative to your choices_are_different() function. It does not add much value otherwise.

"""
if isinstance(a, EnumeratedUserOption):
# If this isn't hte case the above failed somehow
assert isinstance(b, EnumeratedUserOption), 'for mypy'
Copy link
Member

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...

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

mesonbuild/options.py Outdated Show resolved Hide resolved
@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 58437fe to 6b288f8 Compare September 3, 2024 21:20
@dcbaker
Copy link
Member Author

dcbaker commented Sep 3, 2024

@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.

@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 6b288f8 to 60c9113 Compare September 4, 2024 16:34
@bruchar1 bruchar1 added the typing label Sep 6, 2024
Copy link
Member

@bruchar1 bruchar1 left a 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?

mesonbuild/compilers/cpp.py Outdated Show resolved Hide resolved
@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 60c9113 to 0539929 Compare September 6, 2024 19:05
Copy link
Member

@bruchar1 bruchar1 left a 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.',
Copy link
Member

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...

Copy link
Member Author

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}
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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...

Copy link
Member Author

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

mesonbuild/mintro.py Outdated Show resolved Hide resolved
@dcbaker dcbaker force-pushed the submit/fully-type-safe-options branch from 0539929 to 9f0e17d Compare September 6, 2024 19:57
@eli-schwartz
Copy link
Member

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.

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.
Comment on lines 38 to 41
_T = T.TypeVar('_T')
UserOptionType = T.TypeVar('UserOptionType', bound=options.UserOption)

_T = T.TypeVar('_T')

Copy link
Member

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)
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants