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