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

Add cache to OptionKey #14250

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion mesonbuild/coredata.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ def set_options(self, opts_to_set: T.Dict[OptionKey, T.Any], subproject: str = '
# refactor they will get per-subproject values.
really_unknown = []
for uo in unknown_options:
topkey = uo.evolve(subproject='')
topkey = uo.as_toplevel()
if topkey not in self.optstore:
really_unknown.append(uo)
unknown_options = really_unknown
Expand Down
118 changes: 82 additions & 36 deletions mesonbuild/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from __future__ import annotations
from collections import OrderedDict
from itertools import chain
from functools import total_ordering
import argparse
import dataclasses
import itertools
Expand Down Expand Up @@ -105,8 +104,9 @@ class ArgparseKWs(TypedDict, total=False):
}

_BAD_VALUE = 'Qwert Zuiopü'
_OptionKey__cache: T.Dict[T.Tuple[str, str, MachineChoice], OptionKey] = {}


@total_ordering
class OptionKey:

"""Represents an option key in the various option dictionaries.
Expand All @@ -116,26 +116,42 @@ class OptionKey:
internally easier to reason about and produce.
"""

__slots__ = ['name', 'subproject', 'machine', '_hash']
__slots__ = ('name', 'subproject', 'machine', '_hash')

name: str
subproject: T.Optional[str] # None is global, empty string means top level project
subproject: T.Optional[str] # None is global, empty string means top level project
machine: MachineChoice
_hash: int

def __init__(self,
name: str,
subproject: T.Optional[str] = None,
machine: MachineChoice = MachineChoice.HOST):
def __new__(cls,
name: str = '',
subproject: T.Optional[str] = None,
machine: MachineChoice = MachineChoice.HOST) -> OptionKey:
"""The use of the __new__ method allows to add a transparent cache
to the OptionKey object creation, without breaking its API.
"""
if not name:
return super().__new__(cls) # for unpickling, do not cache now

tuple_ = (name, subproject, machine)
try:
return _OptionKey__cache[tuple_]
except KeyError:
instance = super().__new__(cls)
instance._init(name, subproject, machine)
_OptionKey__cache[tuple_] = instance
return instance

def _init(self, name: str, subproject: T.Optional[str], machine: MachineChoice) -> None:
# We don't use the __init__ method, because it would be called after __new__
# while we need __new__ to initialise the object before populating the cache.

if not isinstance(machine, MachineChoice):
raise MesonException(f'Internal error, bad machine type: {machine}')
if not isinstance(name, str):
raise MesonBugException(f'Key name is not a string: {name}')
# the _type option to the constructor is kinda private. We want to be
# able to save the state and avoid the lookup function when
# pickling/unpickling, but we need to be able to calculate it when
# constructing a new OptionKey
assert ':' not in name

object.__setattr__(self, 'name', name)
object.__setattr__(self, 'subproject', subproject)
object.__setattr__(self, 'machine', machine)
Expand All @@ -152,15 +168,9 @@ def __getstate__(self) -> T.Dict[str, T.Any]:
}

def __setstate__(self, state: T.Dict[str, T.Any]) -> None:
"""De-serialize the state of a pickle.

This is very clever. __init__ is not a constructor, it's an
initializer, therefore it's safe to call more than once. We create a
state in the custom __getstate__ method, which is valid to pass
splatted to the initializer.
"""
# Mypy doesn't like this, because it's so clever.
self.__init__(**state) # type: ignore
# Here, the object is created using __new__()
self._init(**state)
_OptionKey__cache[(self.name, self.subproject, self.machine)] = self

def __hash__(self) -> int:
return self._hash
Expand All @@ -173,6 +183,11 @@ def __eq__(self, other: object) -> bool:
return self._to_tuple() == other._to_tuple()
return NotImplemented

def __ne__(self, other: object) -> bool:
if isinstance(other, OptionKey):
return self._to_tuple() != other._to_tuple()
return NotImplemented

def __lt__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None:
Expand All @@ -182,6 +197,33 @@ def __lt__(self, other: object) -> bool:
return self._to_tuple() < other._to_tuple()
return NotImplemented

def __le__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None and other.subproject is not None:
return True
elif self.subproject is not None and other.subproject is None:
return False
return self._to_tuple() <= other._to_tuple()
return NotImplemented

def __gt__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if other.subproject is None:
return self.subproject is not None
elif self.subproject is None:
return False
return self._to_tuple() > other._to_tuple()
return NotImplemented

def __ge__(self, other: object) -> bool:
if isinstance(other, OptionKey):
if self.subproject is None and other.subproject is not None:
return False
elif self.subproject is not None and other.subproject is None:
return True
return self._to_tuple() >= other._to_tuple()
return NotImplemented

def __str__(self) -> str:
out = self.name
if self.machine is MachineChoice.BUILD:
Expand Down Expand Up @@ -240,19 +282,23 @@ def evolve(self,
subproject if subproject != _BAD_VALUE else self.subproject, # None is a valid value so it can'the default value in method declaration.
machine if machine is not None else self.machine)

def as_root(self) -> 'OptionKey':
"""Convenience method for key.evolve(subproject='')."""
if self.subproject is None or self.subproject == '':
def as_root(self) -> OptionKey:
"""Convenience method for key.evolve(subproject='') if subproject is not global."""
if not self.subproject:
return self
return self.evolve(subproject='')
return OptionKey(self.name, subproject='', machine=self.machine)

def as_toplevel(self) -> OptionKey:
"""Convenience method for key.evolve(subproject='')."""
return OptionKey(self.name, subproject='', machine=self.machine)

def as_build(self) -> 'OptionKey':
def as_build(self) -> OptionKey:
"""Convenience method for key.evolve(machine=MachineChoice.BUILD)."""
return self.evolve(machine=MachineChoice.BUILD)
return OptionKey(self.name, self.subproject, machine=MachineChoice.BUILD)

def as_host(self) -> 'OptionKey':
def as_host(self) -> OptionKey:
"""Convenience method for key.evolve(machine=MachineChoice.HOST)."""
return self.evolve(machine=MachineChoice.HOST)
return OptionKey(self.name, self.subproject, machine=MachineChoice.HOST)

def has_module_prefix(self) -> bool:
return '.' in self.name
Expand Down Expand Up @@ -794,7 +840,7 @@ def ensure_and_validate_key(self, key: T.Union[OptionKey, str]) -> OptionKey:
# I did not do this yet, because it would make this MR even
# more massive than it already is. Later then.
if not self.is_cross and key.machine == MachineChoice.BUILD:
key = key.evolve(machine=MachineChoice.HOST)
key = key.as_host()
return key

def get_value(self, key: T.Union[OptionKey, str]) -> 'OptionValueType':
Expand All @@ -809,7 +855,7 @@ def get_value_object_for(self, key: 'T.Union[OptionKey, str]') -> AnyOptionType:
if self.is_project_option(key):
assert key.subproject is not None
if potential is not None and potential.yielding:
parent_key = key.evolve(subproject='')
parent_key = key.as_toplevel()
parent_option = self.options[parent_key]
# If parent object has different type, do not yield.
# This should probably be an error.
Expand All @@ -821,7 +867,7 @@ def get_value_object_for(self, key: 'T.Union[OptionKey, str]') -> AnyOptionType:
return potential
else:
if potential is None:
parent_key = key.evolve(subproject=None)
parent_key = OptionKey(key.name, subproject=None, machine=key.machine)
if parent_key not in self.options:
raise KeyError(f'Tried to access nonexistant project parent option {parent_key}.')
return self.options[parent_key]
Expand Down Expand Up @@ -1036,7 +1082,7 @@ def set_option_from_string(self, keystr: T.Union[OptionKey, str], new_value: str
o = OptionKey.from_string(keystr)
if o in self.options:
return self.set_value(o, new_value)
o = o.evolve(subproject='')
o = o.as_toplevel()
return self.set_value(o, new_value)

def set_subproject_options(self, subproject: str,
Expand Down Expand Up @@ -1289,7 +1335,7 @@ def initialize_from_top_level_project_call(self,
elif key in self.options:
self.set_value(key, valstr, first_invocation)
else:
proj_key = key.evolve(subproject='')
proj_key = key.as_toplevel()
if proj_key in self.options:
self.options[proj_key].set_value(valstr)
else:
Expand Down Expand Up @@ -1322,7 +1368,7 @@ def initialize_from_top_level_project_call(self,
# Argubly this should be a hard error, the default
# value of project option should be set in the option
# file, not in the project call.
proj_key = key.evolve(subproject='')
proj_key = key.as_toplevel()
if self.is_project_option(proj_key):
self.set_option(proj_key, valstr)
else:
Expand All @@ -1340,7 +1386,7 @@ def initialize_from_top_level_project_call(self,
if key in self.options:
self.set_value(key, valstr, True)
elif key.subproject is None:
projectkey = key.evolve(subproject='')
projectkey = key.as_toplevel()
if projectkey in self.options:
self.options[projectkey].set_value(valstr)
else:
Expand Down
Loading