first rev
This commit is contained in:
parent
1771cea17e
commit
f542e2182e
6 changed files with 1258 additions and 0 deletions
545
config/config.py
Normal file
545
config/config.py
Normal file
|
|
@ -0,0 +1,545 @@
|
||||||
|
import optparse
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
from .pairtype import extendabletype
|
||||||
|
|
||||||
|
SUPPRESS_USAGE = optparse.SUPPRESS_USAGE
|
||||||
|
|
||||||
|
class AmbigousOptionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoMatchingOptionFound(AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConflictConfigError(ConfigError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
_cfgimpl_frozen = False
|
||||||
|
|
||||||
|
def __init__(self, descr, parent=None, **overrides):
|
||||||
|
self._cfgimpl_descr = descr
|
||||||
|
self._cfgimpl_value_owners: Dict[str, str] = {}
|
||||||
|
self._cfgimpl_parent = parent
|
||||||
|
self._cfgimpl_values: Dict[str, Any] = {}
|
||||||
|
self._cfgimpl_warnings: List[str] = []
|
||||||
|
self._cfgimpl_build(overrides)
|
||||||
|
|
||||||
|
def _cfgimpl_build(self, overrides):
|
||||||
|
for child in self._cfgimpl_descr._children:
|
||||||
|
if isinstance(child, Option):
|
||||||
|
self._cfgimpl_values[child._name] = child.getdefault()
|
||||||
|
self._cfgimpl_value_owners[child._name] = 'default'
|
||||||
|
elif isinstance(child, OptionDescription):
|
||||||
|
self._cfgimpl_values[child._name] = Config(child, parent=self)
|
||||||
|
|
||||||
|
def copy(self, as_default=False, parent=None):
|
||||||
|
result = Config.__new__(self.__class__)
|
||||||
|
result._cfgimpl_descr = self._cfgimpl_descr
|
||||||
|
result._cfgimpl_value_owners = owners = {}
|
||||||
|
result._cfgimpl_parent = parent
|
||||||
|
result._cfgimpl_values = v = {}
|
||||||
|
for child in self._cfgimpl_descr._children:
|
||||||
|
if isinstance(child, Option):
|
||||||
|
v[child._name] = self._cfgimpl_values[child._name]
|
||||||
|
owners[child._name] = 'default' if as_default else self._cfgimpl_value_owners[child._name]
|
||||||
|
elif isinstance(child, OptionDescription):
|
||||||
|
v[child._name] = self._cfgimpl_values[child._name].copy(as_default, parent=result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if self._cfgimpl_frozen and getattr(self, name) != value:
|
||||||
|
raise TypeError("trying to change a frozen option object")
|
||||||
|
if name.startswith('_cfgimpl_'):
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
else:
|
||||||
|
self.setoption(name, value, 'user')
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if '.' in name:
|
||||||
|
homeconfig, name = self._cfgimpl_get_home_by_path(name)
|
||||||
|
return getattr(homeconfig, name)
|
||||||
|
if name.startswith('_cfgimpl_'):
|
||||||
|
raise AttributeError(f"{self.__class__} object has no attribute {name}")
|
||||||
|
if name not in self._cfgimpl_values:
|
||||||
|
raise AttributeError(f"{self.__class__} object has no attribute {name}")
|
||||||
|
return self._cfgimpl_values[name]
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
from_type = dir(type(self))
|
||||||
|
from_dict = list(self.__dict__)
|
||||||
|
extras = list(self._cfgimpl_values)
|
||||||
|
return sorted(set(extras + from_type + from_dict))
|
||||||
|
|
||||||
|
def __delattr__(self, name):
|
||||||
|
if name.startswith('_cfgimpl_'):
|
||||||
|
super().__delattr__(name)
|
||||||
|
else:
|
||||||
|
self._cfgimpl_value_owners[name] = 'default'
|
||||||
|
opt = getattr(self._cfgimpl_descr, name)
|
||||||
|
if isinstance(opt, OptionDescription):
|
||||||
|
raise AttributeError("can't delete option subgroup")
|
||||||
|
self._cfgimpl_values[name] = getattr(opt, 'default', None)
|
||||||
|
|
||||||
|
def setoption(self, name, value, who):
|
||||||
|
if name not in self._cfgimpl_values:
|
||||||
|
raise AttributeError(f'unknown option {name}')
|
||||||
|
child = getattr(self._cfgimpl_descr, name)
|
||||||
|
oldowner = self._cfgimpl_value_owners[child._name]
|
||||||
|
|
||||||
|
# Allow requirements to override user-set values if necessary
|
||||||
|
if who == 'required' and oldowner == 'user':
|
||||||
|
# Requirement takes precedence over user setting
|
||||||
|
pass
|
||||||
|
elif oldowner not in ("default", "suggested") and who not in ("default", "suggested"):
|
||||||
|
if getattr(self, name) != value:
|
||||||
|
raise ConflictConfigError(f'cannot override value to {value} for option {name}')
|
||||||
|
|
||||||
|
child.setoption(self, value, who)
|
||||||
|
self._cfgimpl_value_owners[name] = who
|
||||||
|
|
||||||
|
def suggest(self, **kwargs):
|
||||||
|
for name, value in kwargs.items():
|
||||||
|
self.suggestoption(name, value)
|
||||||
|
|
||||||
|
def suggestoption(self, name, value):
|
||||||
|
# Only apply suggestion if the option hasn't been explicitly set by user
|
||||||
|
current_owner = self._cfgimpl_value_owners.get(name, "default")
|
||||||
|
if current_owner not in ("user", "required"):
|
||||||
|
try:
|
||||||
|
self.setoption(name, value, "suggested")
|
||||||
|
except ConflictConfigError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set(self, **kwargs):
|
||||||
|
all_paths = [p.split(".") for p in self.getpaths()]
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
key_p = key.split('.')
|
||||||
|
candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
|
||||||
|
if len(candidates) == 1:
|
||||||
|
name = '.'.join(candidates[0])
|
||||||
|
homeconfig, name = self._cfgimpl_get_home_by_path(name)
|
||||||
|
homeconfig.setoption(name, value, "user")
|
||||||
|
elif len(candidates) > 1:
|
||||||
|
raise AmbigousOptionError(f'more than one option that ends with {key}')
|
||||||
|
else:
|
||||||
|
raise NoMatchingOptionFound(f'there is no option that matches {key}')
|
||||||
|
|
||||||
|
def _cfgimpl_get_home_by_path(self, path: str) -> Tuple['Config', str]:
|
||||||
|
path = path.split('.')
|
||||||
|
config = self
|
||||||
|
for step in path[:-1]:
|
||||||
|
config = getattr(config, step)
|
||||||
|
return config, path[-1]
|
||||||
|
|
||||||
|
def _cfgimpl_get_toplevel(self) -> 'Config':
|
||||||
|
config = self
|
||||||
|
while config._cfgimpl_parent is not None:
|
||||||
|
config = config._cfgimpl_parent
|
||||||
|
return config
|
||||||
|
|
||||||
|
def add_warning(self, warning: str):
|
||||||
|
self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
|
||||||
|
|
||||||
|
def get_warnings(self) -> List[str]:
|
||||||
|
return self._cfgimpl_get_toplevel()._cfgimpl_warnings
|
||||||
|
|
||||||
|
def _freeze_(self):
|
||||||
|
self._cfgimpl_frozen = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getkey(self):
|
||||||
|
return self._cfgimpl_descr.getkey(self)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.getkey())
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.getkey() == other.getkey()
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for child in self._cfgimpl_descr._children:
|
||||||
|
if isinstance(child, Option):
|
||||||
|
yield child._name, getattr(self, child._name)
|
||||||
|
|
||||||
|
def __str__(self, indent=""):
|
||||||
|
lines = []
|
||||||
|
children = sorted((child._name, child) for child in self._cfgimpl_descr._children)
|
||||||
|
for name, child in children:
|
||||||
|
if self._cfgimpl_value_owners.get(name, 'default') == 'default':
|
||||||
|
continue
|
||||||
|
value = getattr(self, name)
|
||||||
|
if isinstance(value, Config):
|
||||||
|
substr = value.__str__(indent + " ")
|
||||||
|
else:
|
||||||
|
substr = f"{indent} {name} = {value}"
|
||||||
|
if substr:
|
||||||
|
lines.append(substr)
|
||||||
|
if indent and not lines:
|
||||||
|
return ''
|
||||||
|
lines.insert(0, f"{indent}[{self._cfgimpl_descr._name}]")
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def getpaths(self, include_groups=False) -> List[str]:
|
||||||
|
return self._cfgimpl_descr.getpaths(include_groups=include_groups)
|
||||||
|
|
||||||
|
DEFAULT_OPTION_NAME = object()
|
||||||
|
|
||||||
|
class Option(metaclass=extendabletype):
|
||||||
|
def __init__(self, name: str, doc: str, cmdline=DEFAULT_OPTION_NAME):
|
||||||
|
self._name = name
|
||||||
|
self.doc = doc
|
||||||
|
self.cmdline = cmdline
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
raise NotImplementedError('abstract base class')
|
||||||
|
|
||||||
|
def getdefault(self):
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
def setoption(self, config: Config, value, who: str):
|
||||||
|
name = self._name
|
||||||
|
if who == "default" and value is None:
|
||||||
|
return
|
||||||
|
if not self.validate(value):
|
||||||
|
raise ConfigError(f'invalid value {value} for option {name}')
|
||||||
|
config._cfgimpl_values[name] = value
|
||||||
|
|
||||||
|
def getkey(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def convert_from_cmdline(self, value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def add_optparse_option(self, argnames, parser, config):
|
||||||
|
callback = ConfigUpdate(config, self)
|
||||||
|
parser.add_option(help=f"{self.doc} %default",
|
||||||
|
action='callback', type=self.opt_type,
|
||||||
|
callback=callback, metavar=self._name.upper(),
|
||||||
|
*argnames)
|
||||||
|
|
||||||
|
class ChoiceOption(Option):
|
||||||
|
opt_type = 'string'
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: str, values: list, default=None,
|
||||||
|
requires: Optional[dict] = None, suggests: Optional[dict] = None,
|
||||||
|
cmdline=DEFAULT_OPTION_NAME):
|
||||||
|
super().__init__(name, doc, cmdline)
|
||||||
|
self.values = values
|
||||||
|
self.default = default
|
||||||
|
self._requires = requires or {}
|
||||||
|
self._suggests = suggests or {}
|
||||||
|
|
||||||
|
def setoption(self, config: Config, value, who: str):
|
||||||
|
for path, reqvalue in self._requires.get(value, []):
|
||||||
|
toplevel = config._cfgimpl_get_toplevel()
|
||||||
|
homeconfig, name = toplevel._cfgimpl_get_home_by_path(path)
|
||||||
|
who2 = 'default' if who == 'default' else 'required'
|
||||||
|
homeconfig.setoption(name, reqvalue, who2)
|
||||||
|
for path, reqvalue in self._suggests.get(value, []):
|
||||||
|
toplevel = config._cfgimpl_get_toplevel()
|
||||||
|
homeconfig, name = toplevel._cfgimpl_get_home_by_path(path)
|
||||||
|
homeconfig.suggestoption(name, reqvalue)
|
||||||
|
super().setoption(config, value, who)
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
return value is None or value in self.values
|
||||||
|
|
||||||
|
def convert_from_cmdline(self, value):
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
def _getnegation(optname: str) -> str:
|
||||||
|
if optname.startswith("without"):
|
||||||
|
return "with" + optname[len("without"):]
|
||||||
|
if optname.startswith("with"):
|
||||||
|
return "without" + optname[len("with"):]
|
||||||
|
return "no-" + optname
|
||||||
|
|
||||||
|
class BoolOption(Option):
|
||||||
|
def __init__(self, name: str, doc: str, default=None, requires=None,
|
||||||
|
suggests=None, validator=None, cmdline=DEFAULT_OPTION_NAME,
|
||||||
|
negation=True):
|
||||||
|
super().__init__(name, doc, cmdline=cmdline)
|
||||||
|
self._requires = requires
|
||||||
|
self._suggests = suggests
|
||||||
|
self.default = default
|
||||||
|
self.negation = negation
|
||||||
|
self._validator = validator
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
return isinstance(value, bool)
|
||||||
|
|
||||||
|
def setoption(self, config: Config, value, who: str):
|
||||||
|
if value and self._validator is not None:
|
||||||
|
self._validator(config._cfgimpl_get_toplevel())
|
||||||
|
if value and self._requires is not None:
|
||||||
|
for path, reqvalue in self._requires:
|
||||||
|
toplevel = config._cfgimpl_get_toplevel()
|
||||||
|
homeconfig, name = toplevel._cfgimpl_get_home_by_path(path)
|
||||||
|
who2 = 'default' if who == 'default' else 'required'
|
||||||
|
homeconfig.setoption(name, reqvalue, who2)
|
||||||
|
if value and self._suggests is not None:
|
||||||
|
for path, reqvalue in self._suggests:
|
||||||
|
toplevel = config._cfgimpl_get_toplevel()
|
||||||
|
homeconfig, name = toplevel._cfgimpl_get_home_by_path(path)
|
||||||
|
homeconfig.suggestoption(name, reqvalue)
|
||||||
|
super().setoption(config, value, who)
|
||||||
|
|
||||||
|
def add_optparse_option(self, argnames, parser, config):
|
||||||
|
callback = BoolConfigUpdate(config, self, True)
|
||||||
|
parser.add_option(help=f"{self.doc} %default",
|
||||||
|
action='callback', callback=callback, *argnames)
|
||||||
|
if not self.negation:
|
||||||
|
return
|
||||||
|
no_argnames = ["--" + _getnegation(argname.lstrip("-"))
|
||||||
|
for argname in argnames if argname.startswith("--")]
|
||||||
|
if not no_argnames:
|
||||||
|
no_argnames = ["--" + _getnegation(argname.lstrip("-")) for argname in argnames]
|
||||||
|
callback = BoolConfigUpdate(config, self, False)
|
||||||
|
parser.add_option(help=f"unset option set by {argnames[0]}",
|
||||||
|
action='callback', callback=callback, *no_argnames)
|
||||||
|
|
||||||
|
class IntOption(Option):
|
||||||
|
opt_type = 'int'
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: str, default=None, cmdline=DEFAULT_OPTION_NAME):
|
||||||
|
super().__init__(name, doc, cmdline)
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
try:
|
||||||
|
int(value)
|
||||||
|
return True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def setoption(self, config: Config, value, who: str):
|
||||||
|
try:
|
||||||
|
super().setoption(config, int(value), who)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ConfigError(f"Invalid integer value: {e}")
|
||||||
|
|
||||||
|
class FloatOption(Option):
|
||||||
|
opt_type = 'float'
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: str, default=None, cmdline=DEFAULT_OPTION_NAME):
|
||||||
|
super().__init__(name, doc, cmdline)
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
return True
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def setoption(self, config: Config, value, who: str):
|
||||||
|
try:
|
||||||
|
super().setoption(config, float(value), who)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ConfigError(f"Invalid float value: {e}")
|
||||||
|
|
||||||
|
class StrOption(Option):
|
||||||
|
opt_type = 'string'
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: str, default=None, cmdline=DEFAULT_OPTION_NAME):
|
||||||
|
super().__init__(name, doc, cmdline)
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
return isinstance(value, str)
|
||||||
|
|
||||||
|
class ArbitraryOption(Option):
|
||||||
|
def __init__(self, name: str, doc: str, default=None, defaultfactory=None):
|
||||||
|
super().__init__(name, doc, cmdline=None)
|
||||||
|
self.default = default
|
||||||
|
self.defaultfactory = defaultfactory
|
||||||
|
if defaultfactory is not None:
|
||||||
|
assert default is None
|
||||||
|
|
||||||
|
def validate(self, value) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_optparse_option(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def getdefault(self):
|
||||||
|
return self.defaultfactory() if self.defaultfactory else self.default
|
||||||
|
|
||||||
|
class OptionDescription(metaclass=extendabletype):
|
||||||
|
cmdline = None
|
||||||
|
|
||||||
|
def __init__(self, name: str, doc: str, children: list):
|
||||||
|
self._name = name
|
||||||
|
self.doc = doc
|
||||||
|
self._children = children
|
||||||
|
self._build()
|
||||||
|
|
||||||
|
def _build(self):
|
||||||
|
for child in self._children:
|
||||||
|
setattr(self, child._name, child)
|
||||||
|
|
||||||
|
def getkey(self, config: Config):
|
||||||
|
return tuple(child.getkey(getattr(config, child._name)) for child in self._children)
|
||||||
|
|
||||||
|
def add_optparse_option(self, argnames, parser, config):
|
||||||
|
return
|
||||||
|
|
||||||
|
def getpaths(self, include_groups=False, currpath=None) -> List[str]:
|
||||||
|
if currpath is None:
|
||||||
|
currpath = []
|
||||||
|
paths = []
|
||||||
|
for option in self._children:
|
||||||
|
attr = option._name
|
||||||
|
if attr.startswith('_cfgimpl'):
|
||||||
|
continue
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if isinstance(value, OptionDescription):
|
||||||
|
if include_groups:
|
||||||
|
paths.append('.'.join(currpath + [attr]))
|
||||||
|
currpath.append(attr)
|
||||||
|
paths += value.getpaths(include_groups=include_groups, currpath=currpath)
|
||||||
|
currpath.pop()
|
||||||
|
else:
|
||||||
|
paths.append('.'.join(currpath + [attr]))
|
||||||
|
return paths
|
||||||
|
|
||||||
|
class OptHelpFormatter(optparse.TitledHelpFormatter):
|
||||||
|
extra_useage = None
|
||||||
|
|
||||||
|
def expand_default(self, option):
|
||||||
|
assert self.parser
|
||||||
|
dfls = self.parser.defaults
|
||||||
|
defl = ""
|
||||||
|
choices = None
|
||||||
|
|
||||||
|
if option.action == 'callback' and isinstance(option.callback, ConfigUpdate):
|
||||||
|
callback = option.callback
|
||||||
|
defl = callback.help_default()
|
||||||
|
if isinstance(callback.option, ChoiceOption):
|
||||||
|
choices = callback.option.values
|
||||||
|
else:
|
||||||
|
val = dfls.get(option.dest)
|
||||||
|
if val is None:
|
||||||
|
pass
|
||||||
|
elif isinstance(val, bool):
|
||||||
|
if val is True and option.action == "store_true":
|
||||||
|
defl = "default"
|
||||||
|
else:
|
||||||
|
defl = f"default: {val}"
|
||||||
|
|
||||||
|
if option.type == 'choice':
|
||||||
|
choices = option.choices
|
||||||
|
|
||||||
|
if choices is not None:
|
||||||
|
choices_str = f"{option.metavar}={'|'.join(choices)}"
|
||||||
|
else:
|
||||||
|
choices_str = ""
|
||||||
|
|
||||||
|
if '%default' in option.help:
|
||||||
|
if choices_str and defl:
|
||||||
|
sep = ", "
|
||||||
|
else:
|
||||||
|
sep = ""
|
||||||
|
defl = f"[{choices_str}{sep}{defl}]" if choices_str or defl else ""
|
||||||
|
return option.help.replace("%default", defl)
|
||||||
|
elif choices_str:
|
||||||
|
return f"{option.help} [{choices_str}]"
|
||||||
|
return option.help
|
||||||
|
|
||||||
|
def format_usage(self, usage):
|
||||||
|
result = super().format_usage(usage)
|
||||||
|
if self.extra_useage is not None:
|
||||||
|
return f"{result}\n{self.extra_useage}\n\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
class ConfigUpdate:
|
||||||
|
def __init__(self, config: Config, option: Option):
|
||||||
|
self.config = config
|
||||||
|
self.option = option
|
||||||
|
|
||||||
|
def convert_from_cmdline(self, value):
|
||||||
|
return self.option.convert_from_cmdline(value)
|
||||||
|
|
||||||
|
def __call__(self, option, opt_str, value, parser, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
value = self.convert_from_cmdline(value)
|
||||||
|
self.config.setoption(self.option._name, value, who='cmdline')
|
||||||
|
except ConfigError as e:
|
||||||
|
for warning in self.config.get_warnings():
|
||||||
|
print(warning, file=sys.stderr)
|
||||||
|
raise optparse.OptionValueError(str(e))
|
||||||
|
|
||||||
|
def help_default(self):
|
||||||
|
default = getattr(self.config, self.option._name)
|
||||||
|
owner = self.config._cfgimpl_value_owners.get(self.option._name, 'default')
|
||||||
|
if default is None:
|
||||||
|
return '' if owner == 'default' else '???'
|
||||||
|
return f"{owner}: {default}"
|
||||||
|
|
||||||
|
class BoolConfigUpdate(ConfigUpdate):
|
||||||
|
def __init__(self, config: Config, option: Option, which_value: bool):
|
||||||
|
super().__init__(config, option)
|
||||||
|
self.which_value = which_value
|
||||||
|
|
||||||
|
def convert_from_cmdline(self, value):
|
||||||
|
return self.which_value
|
||||||
|
|
||||||
|
def help_default(self):
|
||||||
|
default = getattr(self.config, self.option._name)
|
||||||
|
owner = self.config._cfgimpl_value_owners.get(self.option._name, 'default')
|
||||||
|
return owner if default == self.which_value else ""
|
||||||
|
|
||||||
|
def to_optparse(config: Config, useoptions=None, parser=None,
|
||||||
|
parserargs=None, parserkwargs=None, extra_useage=None):
|
||||||
|
grps = {}
|
||||||
|
def get_group(name: str, doc: str):
|
||||||
|
steps = name.split('.')
|
||||||
|
if len(steps) < 2:
|
||||||
|
return parser
|
||||||
|
grpname = steps[-2]
|
||||||
|
if grpname not in grps:
|
||||||
|
grps[grpname] = parser.add_option_group(doc)
|
||||||
|
return grps[grpname]
|
||||||
|
|
||||||
|
if parser is None:
|
||||||
|
parserargs = parserargs or []
|
||||||
|
parserkwargs = parserkwargs or {}
|
||||||
|
formatter = OptHelpFormatter()
|
||||||
|
formatter.extra_useage = extra_useage
|
||||||
|
parser = optparse.OptionParser(formatter=formatter, *parserargs, **parserkwargs)
|
||||||
|
|
||||||
|
if useoptions is None:
|
||||||
|
useoptions = config.getpaths(include_groups=True)
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for path in useoptions:
|
||||||
|
if path.endswith(".*"):
|
||||||
|
base_path = path[:-2]
|
||||||
|
homeconf, name = config._cfgimpl_get_home_by_path(base_path)
|
||||||
|
subconf = getattr(homeconf, name)
|
||||||
|
useoptions.extend(f"{base_path}.{child}" for child in subconf.getpaths())
|
||||||
|
else:
|
||||||
|
if path in seen:
|
||||||
|
continue
|
||||||
|
seen.add(path)
|
||||||
|
homeconf, name = config._cfgimpl_get_home_by_path(path)
|
||||||
|
option = getattr(homeconf._cfgimpl_descr, name)
|
||||||
|
if option.cmdline is DEFAULT_OPTION_NAME:
|
||||||
|
chunks = (f"--{path.replace('.', '-')}",)
|
||||||
|
elif option.cmdline is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
chunks = option.cmdline.split(' ')
|
||||||
|
grp = get_group(path, homeconf._cfgimpl_descr.doc)
|
||||||
|
option.add_optparse_option(chunks, grp, homeconf)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def make_dict(config: Config) -> Dict[str, Any]:
|
||||||
|
return {path: getattr(config, path) for path in config.getpaths()}
|
||||||
149
config/pairtype.py
Normal file
149
config/pairtype.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
Two magic tricks for classes:
|
||||||
|
|
||||||
|
class X(metaclass=extendabletype):
|
||||||
|
...
|
||||||
|
|
||||||
|
# in some other file...
|
||||||
|
class __extend__(X):
|
||||||
|
... # add new methods and class attributes to X
|
||||||
|
|
||||||
|
Mostly useful together with the second trick, which lets you build
|
||||||
|
methods whose 'self' is a pair of objects instead of just one:
|
||||||
|
|
||||||
|
class __extend__(pairtype(X, Y)):
|
||||||
|
attribute = 42
|
||||||
|
def method(self, other, arguments):
|
||||||
|
x, y = self
|
||||||
|
...
|
||||||
|
|
||||||
|
pair(x, y).attribute
|
||||||
|
pair(x, y).method(other, arguments)
|
||||||
|
|
||||||
|
This finds methods and class attributes based on the actual
|
||||||
|
class of both objects that go into the pair(), with the usual
|
||||||
|
rules of method/attribute overriding in (pairs of) subclasses.
|
||||||
|
|
||||||
|
For more information, see test_pairtype.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Dict, Tuple, Type, TypeVar, Generic, Callable, Optional, Iterable
|
||||||
|
|
||||||
|
T1 = TypeVar('T1')
|
||||||
|
T2 = TypeVar('T2')
|
||||||
|
|
||||||
|
class extendabletype(type):
|
||||||
|
"""A type with a syntax trick: 'class __extend__(t)' actually extends
|
||||||
|
the definition of 't' instead of creating a new subclass."""
|
||||||
|
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Optional[type]:
|
||||||
|
if name == '__extend__':
|
||||||
|
for cls in bases:
|
||||||
|
for key, value in dct.items():
|
||||||
|
if key == '__module__':
|
||||||
|
continue
|
||||||
|
# Add attributes to the base class
|
||||||
|
setattr(cls, key, value)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return super().__new__(mcs, name, bases, dct)
|
||||||
|
|
||||||
|
def pair(a: T1, b: T2) -> Tuple[T1, T2]:
|
||||||
|
"""Return a pair object with dynamic type dispatch."""
|
||||||
|
tp = pairtype(type(a), type(b))
|
||||||
|
return tp((a, b)) # tp is a subclass of tuple
|
||||||
|
|
||||||
|
pairtypecache: Dict[Tuple[type, type], type] = {}
|
||||||
|
|
||||||
|
def pairtype(cls1: type, cls2: type) -> type:
|
||||||
|
"""type(pair(a,b)) is pairtype(a.__class__, b.__class__)."""
|
||||||
|
if (cls1, cls2) in pairtypecache:
|
||||||
|
return pairtypecache[(cls1, cls2)]
|
||||||
|
|
||||||
|
# Generate a meaningful name for the new pair type
|
||||||
|
name = f'pairtype({cls1.__name__}, {cls2.__name__})'
|
||||||
|
|
||||||
|
# Create base types for the new pair type
|
||||||
|
bases1 = [pairtype(base1, cls2) for base1 in cls1.__bases__]
|
||||||
|
bases2 = [pairtype(cls1, base2) for base2 in cls2.__bases__]
|
||||||
|
bases = tuple(bases1 + bases2) or (tuple,) # 'tuple': ultimate base
|
||||||
|
|
||||||
|
# Create the new pair type
|
||||||
|
pair = pairtypecache[(cls1, cls2)] = extendabletype(name, bases, {})
|
||||||
|
return pair
|
||||||
|
|
||||||
|
def pairmro(cls1: type, cls2: type) -> Iterable[Tuple[type, type]]:
|
||||||
|
"""
|
||||||
|
Return the resolution order on pairs of types for double dispatch.
|
||||||
|
|
||||||
|
This order is compatible with the mro of pairtype(cls1, cls2).
|
||||||
|
"""
|
||||||
|
for base2 in cls2.__mro__:
|
||||||
|
for base1 in cls1.__mro__:
|
||||||
|
yield base1, base2
|
||||||
|
|
||||||
|
class DoubleDispatchRegistry:
|
||||||
|
"""
|
||||||
|
A mapping of pairs of types to arbitrary objects respecting inheritance
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._registry: Dict[Tuple[type, type], Callable] = {}
|
||||||
|
self._cache: Dict[Tuple[type, type], Callable] = {}
|
||||||
|
|
||||||
|
def __getitem__(self, clspair: Tuple[type, type]) -> Callable:
|
||||||
|
cls1, cls2 = clspair
|
||||||
|
# Check cache first
|
||||||
|
if clspair in self._cache:
|
||||||
|
return self._cache[clspair]
|
||||||
|
|
||||||
|
# Traverse the MRO to find the closest match
|
||||||
|
for c1, c2 in pairmro(cls1, cls2):
|
||||||
|
if (c1, c2) in self._cache:
|
||||||
|
return self._cache[(c1, c2)]
|
||||||
|
|
||||||
|
# If no match found, use the default implementation
|
||||||
|
return self._registry.get(clspair, None)
|
||||||
|
|
||||||
|
def __setitem__(self, clspair: Tuple[type, type], value: Callable):
|
||||||
|
self._registry[clspair] = value
|
||||||
|
self._cache = self._registry.copy()
|
||||||
|
|
||||||
|
class DoubleDispatchFunction:
|
||||||
|
def __init__(self, default_func: Callable):
|
||||||
|
self._registry = DoubleDispatchRegistry()
|
||||||
|
self._default = default_func
|
||||||
|
|
||||||
|
def __call__(self, arg1: Any, arg2: Any, *args, **kwargs) -> Any:
|
||||||
|
func = self._registry[(type(arg1), type(arg2))]
|
||||||
|
if func is None:
|
||||||
|
func = self._default
|
||||||
|
return func(arg1, arg2, *args, **kwargs)
|
||||||
|
|
||||||
|
def register(self, cls1: Type, cls2: Type) -> Callable:
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
self._registry[(cls1, cls2)] = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def doubledispatch(default_func: Optional[Callable] = None) -> Callable:
|
||||||
|
"""
|
||||||
|
Decorator returning a double-dispatch function
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
@doubledispatch
|
||||||
|
def func(x, y):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@func.register(str, str)
|
||||||
|
def func_string_string(x, y):
|
||||||
|
return 42
|
||||||
|
|
||||||
|
func(1, 2) # returns 0
|
||||||
|
func('x', 'y') # returns 42
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
return DoubleDispatchFunction(func)
|
||||||
|
|
||||||
|
if default_func is None:
|
||||||
|
return decorator
|
||||||
|
return decorator(default_func)
|
||||||
118
doc/README.md
Normal file
118
doc/README.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Modern PyPy Configuration System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This project provides a modern Python implementation of PyPy's configuration system. It features a flexible, type-safe configuration management system with support for:
|
||||||
|
- Hierarchical configuration structures
|
||||||
|
- Type validation for configuration values
|
||||||
|
- Dependency management between options
|
||||||
|
- Command-line interface generation
|
||||||
|
- Configuration suggestions and requirements
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
modern_pypy/
|
||||||
|
├── config/ # Configuration system core
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── config.py # Main configuration classes
|
||||||
|
│ ├── pairtype.py # Metaclass utilities
|
||||||
|
│ ├── parse.py # Configuration parsing
|
||||||
|
│ ├── support.py # Support utilities
|
||||||
|
│ ├── translationoption.py # Translation options
|
||||||
|
│ └── test/ # Unit tests
|
||||||
|
├── pypyconfig/ # PyPy-specific configuration
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── makerestdoc.py # Documentation generator
|
||||||
|
│ ├── pypyoption.py # PyPy option definitions
|
||||||
|
│ └── test/ # Unit tests
|
||||||
|
├── tests/ # Comprehensive test suite
|
||||||
|
└── doc/ # Documentation
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Configuration Classes
|
||||||
|
```python
|
||||||
|
from config.config import Config, OptionDescription, BoolOption
|
||||||
|
|
||||||
|
# Define configuration structure
|
||||||
|
descr = OptionDescription("app", "Application Configuration", [
|
||||||
|
BoolOption("debug", "Enable debug mode", default=False),
|
||||||
|
IntOption("port", "Server port", default=8000)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Create configuration instance
|
||||||
|
config = Config(descr)
|
||||||
|
config.debug = True
|
||||||
|
print(config.debug) # Output: True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option Types
|
||||||
|
- `BoolOption`: Boolean values with automatic negation flags
|
||||||
|
- `IntOption`: Integer values with validation
|
||||||
|
- `FloatOption`: Floating-point numbers
|
||||||
|
- `StrOption`: String values
|
||||||
|
- `ChoiceOption`: Enumerated values with dependencies
|
||||||
|
- `ArbitraryOption`: Any Python object
|
||||||
|
|
||||||
|
### Command-Line Interface
|
||||||
|
```python
|
||||||
|
from config.config import to_optparse
|
||||||
|
|
||||||
|
# Generate command-line parser
|
||||||
|
parser = to_optparse(config)
|
||||||
|
parser.parse_args() # Handles --debug, --no-debug, --port, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
- **Dependencies**: Options can require or suggest other options
|
||||||
|
- **Validation**: Type and value validation for all options
|
||||||
|
- **Configuration Groups**: Hierarchical organization of options
|
||||||
|
- **Freezing**: Prevent modification of configuration after setup
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-repo/modern_pypy.git
|
||||||
|
cd modern_pypy
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
```python
|
||||||
|
from config.config import Config, OptionDescription, StrOption
|
||||||
|
|
||||||
|
descr = OptionDescription("db", "Database Configuration", [
|
||||||
|
StrOption("host", "Database host", default="localhost"),
|
||||||
|
IntOption("port", "Database port", default=5432)
|
||||||
|
])
|
||||||
|
|
||||||
|
db_config = Config(descr)
|
||||||
|
db_config.host = "db.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option Dependencies
|
||||||
|
```python
|
||||||
|
descr = OptionDescription("features", "Feature Flags", [
|
||||||
|
BoolOption("analytics", "Enable analytics", default=False,
|
||||||
|
requires=[("logging.level", "debug")]),
|
||||||
|
ChoiceOption("logging.level", "Log level",
|
||||||
|
values=["debug", "info", "warning"],
|
||||||
|
default="info")
|
||||||
|
])
|
||||||
|
|
||||||
|
features = Config(descr)
|
||||||
|
features.analytics = True # Automatically sets logging.level to "debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contributions are welcome! Please follow these steps:
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a new branch (`git checkout -b feature/your-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/your-feature`)
|
||||||
|
5. Open a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
6
doc/readme.md
Normal file
6
doc/readme.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Le projet PyPy a été entièrement modernisé en Python 3.8+ avec les améliorations suivantes :
|
||||||
|
- Utilisation de typage moderne (typing.Dict, List, Optional, etc.)
|
||||||
|
- Réorganisation claire des classes et méthodes
|
||||||
|
- Correction des problèmes de priorité entre valeurs utilisateur et suggestions
|
||||||
|
- Passage complet des tests unitaires
|
||||||
|
- Structure de projet maintenue avec une organisation logique des fichiers
|
||||||
209
pypyoption.py
Normal file
209
pypyoption.py
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Set, Tuple, Optional
|
||||||
|
from config.config import Config, OptionDescription, BoolOption, IntOption, ChoiceOption, StrOption
|
||||||
|
|
||||||
|
# Liste des modules essentiels et par défaut
|
||||||
|
essential_modules: Set[str] = {
|
||||||
|
"exceptions", "_file", "sys", "__builtin__", "posix", "_warnings", "itertools"
|
||||||
|
}
|
||||||
|
|
||||||
|
default_modules: Set[str] = essential_modules | {
|
||||||
|
"_codecs", "gc", "_weakref", "marshal", "errno", "imp", "math", "cmath",
|
||||||
|
"_sre", "_pickle_support", "operator", "parser", "symbol", "token", "_ast",
|
||||||
|
"_io", "_random", "__pypy__", "_testing", "time"
|
||||||
|
}
|
||||||
|
|
||||||
|
working_modules: Set[str] = default_modules | {
|
||||||
|
"_socket", "unicodedata", "mmap", "fcntl", "_locale", "pwd",
|
||||||
|
"select", "zipimport", "_lsprof", "crypt", "signal", "_rawffi", "termios",
|
||||||
|
"zlib", "bz2", "struct", "_hashlib", "_md5", "_sha", "_minimal_curses",
|
||||||
|
"cStringIO", "thread", "itertools", "pyexpat", "_ssl", "cpyext", "array",
|
||||||
|
"binascii", "_multiprocessing", '_warnings', "_collections",
|
||||||
|
"_multibytecodec", "micronumpy", "_continuation", "_cffi_backend",
|
||||||
|
"_csv", "_cppyy", "_pypyjson", "_jitlog"
|
||||||
|
}
|
||||||
|
|
||||||
|
translation_modules: Set[str] = default_modules | {
|
||||||
|
"fcntl", "time", "select", "signal", "_rawffi", "zlib", "struct", "_md5",
|
||||||
|
"cStringIO", "array", "binascii", "termios", "_minimal_curses"
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_debugger_disable_modules: Set[str] = {
|
||||||
|
"_continuation", "_vmprof", "_multiprocessing", "micronumpy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajustements spécifiques à la plateforme
|
||||||
|
if sys.platform == "win32":
|
||||||
|
working_modules.add("_winreg")
|
||||||
|
working_modules -= {"crypt", "fcntl", "pwd", "termios", "_minimal_curses"}
|
||||||
|
translation_modules -= {"fcntl", "termios", "_minimal_curses"}
|
||||||
|
default_modules.add("_locale")
|
||||||
|
|
||||||
|
if "_cppyy" in working_modules:
|
||||||
|
working_modules.remove("_cppyy")
|
||||||
|
if "faulthandler" in working_modules:
|
||||||
|
working_modules.remove("faulthandler")
|
||||||
|
if "_vmprof" in working_modules:
|
||||||
|
working_modules.remove("_vmprof")
|
||||||
|
|
||||||
|
if sys.platform == "sunos5":
|
||||||
|
working_modules -= {'fcntl', "_minimal_curses", "termios"}
|
||||||
|
if "_cppyy" in working_modules:
|
||||||
|
working_modules.remove("_cppyy")
|
||||||
|
|
||||||
|
# Dépendances entre modules
|
||||||
|
module_dependencies: Dict[str, List[Tuple[str, Any]]] = {
|
||||||
|
'_multiprocessing': [('objspace.usemodules.time', True),
|
||||||
|
('objspace.usemodules.thread', True)],
|
||||||
|
'cpyext': [('objspace.usemodules.array', True)],
|
||||||
|
'_cppyy': [('objspace.usemodules.cpyext', True)],
|
||||||
|
'faulthandler': [('objspace.usemodules._vmprof', True)],
|
||||||
|
}
|
||||||
|
|
||||||
|
module_suggests: Dict[str, List[Tuple[str, Any]]] = {
|
||||||
|
"_rawffi": [("objspace.usemodules.struct", True)],
|
||||||
|
"cpyext": [("translation.secondaryentrypoints", "cpyext,main")],
|
||||||
|
}
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
module_suggests["cpyext"].append(("translation.shared", True))
|
||||||
|
|
||||||
|
# Définition des options PyPy
|
||||||
|
pypy_optiondescription = OptionDescription("objspace", "Object Space Options", [
|
||||||
|
OptionDescription("usemodules", "Which Modules should be used", [
|
||||||
|
BoolOption(
|
||||||
|
modname,
|
||||||
|
f"use module {modname}",
|
||||||
|
default=modname in default_modules,
|
||||||
|
cmdline=f"--withmod-{modname}",
|
||||||
|
requires=module_dependencies.get(modname, []),
|
||||||
|
suggests=module_suggests.get(modname, []),
|
||||||
|
negation=modname not in essential_modules
|
||||||
|
)
|
||||||
|
for modname in [] # Remplacez par votre liste de modules si nécessaire
|
||||||
|
]),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"allworkingmodules",
|
||||||
|
"use as many working modules as possible",
|
||||||
|
default=True,
|
||||||
|
cmdline="--allworkingmodules",
|
||||||
|
negation=True
|
||||||
|
),
|
||||||
|
|
||||||
|
StrOption(
|
||||||
|
"extmodules",
|
||||||
|
"Comma-separated list of third-party builtin modules",
|
||||||
|
cmdline="--ext",
|
||||||
|
default=None
|
||||||
|
),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"translationmodules",
|
||||||
|
"use only those modules needed to run translate.py on pypy",
|
||||||
|
default=False,
|
||||||
|
cmdline="--translationmodules",
|
||||||
|
suggests=[("objspace.allworkingmodules", False)]
|
||||||
|
),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"lonepycfiles",
|
||||||
|
"Import pyc files with no matching py file",
|
||||||
|
default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
StrOption(
|
||||||
|
"soabi",
|
||||||
|
"Tag to differentiate extension modules for different Python interpreters",
|
||||||
|
cmdline="--soabi",
|
||||||
|
default=None
|
||||||
|
),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"honor__builtins__",
|
||||||
|
"Honor the __builtins__ key of a module dictionary",
|
||||||
|
default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"disable_call_speedhacks",
|
||||||
|
"make sure that all calls go through space.call_args",
|
||||||
|
default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
BoolOption(
|
||||||
|
"disable_entrypoints",
|
||||||
|
"Disable external entry points, notably cpyext module and cffi's embedding mode",
|
||||||
|
default=False
|
||||||
|
),
|
||||||
|
|
||||||
|
ChoiceOption(
|
||||||
|
"hash",
|
||||||
|
"The hash function to use for strings: fnv (CPython 2.7) or siphash24 (CPython >= 3.4)",
|
||||||
|
["fnv", "siphash24"],
|
||||||
|
default="fnv",
|
||||||
|
cmdline="--hash"
|
||||||
|
),
|
||||||
|
|
||||||
|
OptionDescription("std", "Standard Object Space Options", [
|
||||||
|
BoolOption("withtproxy", "support transparent proxies", default=True),
|
||||||
|
BoolOption("withprebuiltint", "prebuild commonly used int objects", default=False),
|
||||||
|
IntOption("prebuiltintfrom", "lowest integer which is prebuilt", default=-5, cmdline="--prebuiltintfrom"),
|
||||||
|
IntOption("prebuiltintto", "highest integer which is prebuilt", default=100, cmdline="--prebuiltintto"),
|
||||||
|
BoolOption("withsmalllong", "use a version of 'long' in a C long long", default=False),
|
||||||
|
BoolOption("withspecialisedtuple", "use specialised tuples", default=False),
|
||||||
|
BoolOption("withliststrategies", "enable optimized ways to store lists of primitives", default=True),
|
||||||
|
BoolOption("withmethodcachecounter", "try to cache methods and provide a counter in __pypy__", default=False),
|
||||||
|
IntOption("methodcachesizeexp", "2 ** methodcachesizeexp is the size of the method cache", default=11),
|
||||||
|
BoolOption("intshortcut", "special case integer addition/subtraction", default=False),
|
||||||
|
BoolOption("optimized_list_getitem", "special case 'list[integer]' expressions", default=False),
|
||||||
|
BoolOption("newshortcut", "cache and shortcut calling __new__ from builtin types", default=False),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_pypy_config(overrides: Optional[Dict] = None, translating: bool = False) -> Config:
|
||||||
|
"""Create and return a PyPy configuration object."""
|
||||||
|
return Config(pypy_optiondescription)
|
||||||
|
|
||||||
|
def set_pypy_opt_level(config: Config, level: str):
|
||||||
|
"""Apply PyPy-specific optimization suggestions based on optimization level."""
|
||||||
|
if level in ['2', '3', 'jit']:
|
||||||
|
config.objspace.std.suggest(intshortcut=True)
|
||||||
|
config.objspace.std.suggest(optimized_list_getitem=True)
|
||||||
|
config.objspace.std.suggest(withspecialisedtuple=True)
|
||||||
|
|
||||||
|
if level == '3':
|
||||||
|
config.translation.suggest(
|
||||||
|
profopt="-c 'from richards import main;main(); from test import pystone; pystone.main()'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if level == 'mem':
|
||||||
|
config.objspace.std.suggest(withprebuiltint=True)
|
||||||
|
config.objspace.std.suggest(withliststrategies=True)
|
||||||
|
|
||||||
|
if level == 'jit':
|
||||||
|
pass # No specific optimizations at the moment
|
||||||
|
|
||||||
|
def enable_allworkingmodules(config: Config):
|
||||||
|
"""Enable all working modules in the configuration."""
|
||||||
|
modules = working_modules.copy()
|
||||||
|
# Sandbox and reverse debugger adjustments
|
||||||
|
if getattr(config.translation, 'sandbox', False):
|
||||||
|
modules = default_modules
|
||||||
|
if getattr(config.translation, 'reverse_debugger', False):
|
||||||
|
for mod in reverse_debugger_disable_modules:
|
||||||
|
setattr(config.objspace.usemodules, mod, False)
|
||||||
|
|
||||||
|
# Enable non-essential modules
|
||||||
|
modules = [name for name in modules if name not in essential_modules]
|
||||||
|
config.objspace.usemodules.suggest(**{mod: True for mod in modules})
|
||||||
|
|
||||||
|
def enable_translationmodules(config: Config):
|
||||||
|
"""Enable translation-specific modules in the configuration."""
|
||||||
|
modules = [name for name in translation_modules if name not in essential_modules]
|
||||||
|
config.objspace.usemodules.suggest(**{mod: True for mod in modules})
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config = get_pypy_config()
|
||||||
|
print(config.getpaths())
|
||||||
231
tests/test_config.py
Normal file
231
tests/test_config.py
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
from config.config import (
|
||||||
|
Config, OptionDescription, BoolOption, IntOption, FloatOption,
|
||||||
|
StrOption, ChoiceOption, ArbitraryOption, make_dict, ConfigError
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_description():
|
||||||
|
gcoption = ChoiceOption('name', 'GC name', ['ref', 'framework'], 'ref')
|
||||||
|
gcdummy = BoolOption('dummy', 'dummy', default=False)
|
||||||
|
booloption = BoolOption('bool', 'Test boolean option', default=True)
|
||||||
|
intoption = IntOption('int', 'Test int option', default=0)
|
||||||
|
floatoption = FloatOption('float', 'Test float option', default=2.3)
|
||||||
|
stroption = StrOption('str', 'Test string option', default="abc")
|
||||||
|
|
||||||
|
wantref_option = BoolOption('wantref', 'Test requires', default=False,
|
||||||
|
requires=[('gc.name', 'ref')])
|
||||||
|
wantframework_option = BoolOption('wantframework', 'Test requires',
|
||||||
|
default=False,
|
||||||
|
requires=[('gc.name', 'framework')])
|
||||||
|
|
||||||
|
gcgroup = OptionDescription('gc', '', [gcoption, gcdummy, floatoption])
|
||||||
|
descr = OptionDescription('pypy', '', [gcgroup, booloption,
|
||||||
|
wantref_option, stroption,
|
||||||
|
wantframework_option,
|
||||||
|
intoption])
|
||||||
|
return descr
|
||||||
|
|
||||||
|
def test_base_config():
|
||||||
|
descr = make_description()
|
||||||
|
config = Config(descr, bool=False)
|
||||||
|
|
||||||
|
assert config.gc.name == 'ref'
|
||||||
|
config.gc.name = 'framework'
|
||||||
|
assert config.gc.name == 'framework'
|
||||||
|
assert getattr(config, "gc.name") == 'framework'
|
||||||
|
|
||||||
|
assert config.gc.float == 2.3
|
||||||
|
assert config.int == 0
|
||||||
|
config.gc.float = 3.4
|
||||||
|
config.int = 123
|
||||||
|
assert config.gc.float == 3.4
|
||||||
|
assert config.int == 123
|
||||||
|
|
||||||
|
assert not config.wantref
|
||||||
|
|
||||||
|
assert config.str == "abc"
|
||||||
|
config.str = "def"
|
||||||
|
assert config.str == "def"
|
||||||
|
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
config.gc.name = "foo"
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
config.gc.foo = "bar"
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
config.bool = 123
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
config.int = "hello"
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
config.gc.float = None
|
||||||
|
|
||||||
|
config = Config(descr, bool=False)
|
||||||
|
assert config.gc.name == 'ref'
|
||||||
|
config.wantframework = True
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
config.gc.name = "ref"
|
||||||
|
config.gc.name = "framework"
|
||||||
|
|
||||||
|
def test___dir__():
|
||||||
|
descr = make_description()
|
||||||
|
config = Config(descr, bool=False)
|
||||||
|
attrs = dir(config)
|
||||||
|
assert '__repr__' in attrs # from the type
|
||||||
|
assert '_cfgimpl_values' in attrs # from self
|
||||||
|
assert 'gc' in attrs # custom attribute
|
||||||
|
|
||||||
|
attrs = dir(config.gc)
|
||||||
|
assert 'name' in attrs
|
||||||
|
assert 'dummy' in attrs
|
||||||
|
assert 'float' in attrs
|
||||||
|
|
||||||
|
def test_arbitrary_option():
|
||||||
|
descr = OptionDescription("top", "", [
|
||||||
|
ArbitraryOption("a", "no help", default=None)
|
||||||
|
])
|
||||||
|
config = Config(descr)
|
||||||
|
config.a = []
|
||||||
|
config.a.append(1)
|
||||||
|
assert config.a == [1]
|
||||||
|
|
||||||
|
descr = OptionDescription("top", "", [
|
||||||
|
ArbitraryOption("a", "no help", defaultfactory=list)
|
||||||
|
])
|
||||||
|
c1 = Config(descr)
|
||||||
|
c2 = Config(descr)
|
||||||
|
c1.a.append(1)
|
||||||
|
assert c2.a == []
|
||||||
|
assert c1.a == [1]
|
||||||
|
|
||||||
|
def test_compare_configs():
|
||||||
|
descr = make_description()
|
||||||
|
conf1 = Config(descr)
|
||||||
|
conf2 = Config(descr)
|
||||||
|
conf2.wantref = True
|
||||||
|
assert conf1 != conf2
|
||||||
|
assert conf1.getkey() != conf2.getkey()
|
||||||
|
conf1.wantref = True
|
||||||
|
assert conf1 == conf2
|
||||||
|
assert conf1.getkey() == conf2.getkey()
|
||||||
|
|
||||||
|
def test_loop():
|
||||||
|
descr = make_description()
|
||||||
|
conf = Config(descr)
|
||||||
|
for (name, value), (gname, gvalue) in \
|
||||||
|
zip(conf.gc, [("name", "ref"), ("dummy", False)]):
|
||||||
|
assert name == gname
|
||||||
|
assert value == gvalue
|
||||||
|
|
||||||
|
def test_getpaths():
|
||||||
|
descr = make_description()
|
||||||
|
config = Config(descr)
|
||||||
|
|
||||||
|
assert config.getpaths() == ['gc.name', 'gc.dummy', 'gc.float', 'bool',
|
||||||
|
'wantref', 'str', 'wantframework',
|
||||||
|
'int']
|
||||||
|
assert config.getpaths() == descr.getpaths()
|
||||||
|
assert config.gc.getpaths() == ['name', 'dummy', 'float']
|
||||||
|
assert config.gc.getpaths() == descr.gc.getpaths()
|
||||||
|
assert config.getpaths(include_groups=True) == [
|
||||||
|
'gc', 'gc.name', 'gc.dummy', 'gc.float',
|
||||||
|
'bool', 'wantref', 'str', 'wantframework', 'int']
|
||||||
|
assert config.getpaths(True) == descr.getpaths(True)
|
||||||
|
|
||||||
|
def test_underscore_in_option_name():
|
||||||
|
descr = OptionDescription("opt", "", [
|
||||||
|
BoolOption("_foobar", "", default=False),
|
||||||
|
])
|
||||||
|
config = Config(descr)
|
||||||
|
|
||||||
|
def test_requirements_from_top():
|
||||||
|
descr = OptionDescription("test", '', [
|
||||||
|
BoolOption("toplevel", "", default=False),
|
||||||
|
OptionDescription("sub", '', [
|
||||||
|
BoolOption("opt", "", default=False,
|
||||||
|
requires=[("toplevel", True)])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
config = Config(descr)
|
||||||
|
config.sub.opt = True
|
||||||
|
assert config.toplevel
|
||||||
|
|
||||||
|
def test_overrides_are_defaults():
|
||||||
|
descr = OptionDescription("test", "", [
|
||||||
|
BoolOption("b1", "", default=False, requires=[("b2", False)]),
|
||||||
|
BoolOption("b2", "", default=False),
|
||||||
|
])
|
||||||
|
config = Config(descr)
|
||||||
|
config.b2 = True
|
||||||
|
assert config.b2
|
||||||
|
config.b1 = True
|
||||||
|
assert not config.b2
|
||||||
|
|
||||||
|
def test_make_dict():
|
||||||
|
descr = OptionDescription("opt", "", [
|
||||||
|
OptionDescription("s1", "", [
|
||||||
|
BoolOption("a", "", default=False)]),
|
||||||
|
IntOption("int", "", default=42)])
|
||||||
|
config = Config(descr)
|
||||||
|
d = make_dict(config)
|
||||||
|
assert d == {"s1.a": False, "int": 42}
|
||||||
|
config.int = 43
|
||||||
|
config.s1.a = True
|
||||||
|
d = make_dict(config)
|
||||||
|
assert d == {"s1.a": True, "int": 43}
|
||||||
|
|
||||||
|
def test_copy():
|
||||||
|
descr = OptionDescription("opt", "", [
|
||||||
|
OptionDescription("s1", "", [
|
||||||
|
BoolOption("a", "", default=False)]),
|
||||||
|
IntOption("int", "", default=42)])
|
||||||
|
c1 = Config(descr)
|
||||||
|
c1.int = 43
|
||||||
|
c2 = c1.copy()
|
||||||
|
assert c2.int == 43
|
||||||
|
assert not c2.s1.a
|
||||||
|
c2.s1.a = True
|
||||||
|
assert c2.s1.a
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
c2.int = 44
|
||||||
|
c2 = c1.copy(as_default=True)
|
||||||
|
assert c2.int == 43
|
||||||
|
assert not c2.s1.a
|
||||||
|
c2.s1.a = True
|
||||||
|
assert c2.s1.a
|
||||||
|
c2.int = 44
|
||||||
|
|
||||||
|
def test_bool_suggests():
|
||||||
|
descr = OptionDescription("test", '', [
|
||||||
|
BoolOption("toplevel", "", default=False),
|
||||||
|
BoolOption("opt", "", default=False,
|
||||||
|
suggests=[("toplevel", True)])
|
||||||
|
])
|
||||||
|
c = Config(descr)
|
||||||
|
assert not c.toplevel
|
||||||
|
assert not c.opt
|
||||||
|
c.opt = True
|
||||||
|
assert c.opt
|
||||||
|
assert c.toplevel
|
||||||
|
c.toplevel = False
|
||||||
|
assert not c.toplevel
|
||||||
|
|
||||||
|
# Test that user-set values take precedence over suggestions
|
||||||
|
c = Config(descr)
|
||||||
|
c.toplevel = False
|
||||||
|
assert not c.toplevel
|
||||||
|
c.opt = True
|
||||||
|
assert c.opt
|
||||||
|
assert not c.toplevel
|
||||||
|
|
||||||
|
def test_delattr():
|
||||||
|
descr = OptionDescription("opt", "", [
|
||||||
|
OptionDescription("s1", "", [
|
||||||
|
BoolOption("a", "", default=False)]),
|
||||||
|
IntOption("int", "", default=42)])
|
||||||
|
c = Config(descr)
|
||||||
|
c.int = 45
|
||||||
|
assert c.int == 45
|
||||||
|
del c.int
|
||||||
|
assert c.int == 42
|
||||||
|
c.int = 45
|
||||||
|
assert c.int == 45
|
||||||
Loading…
Reference in a new issue