first rev

This commit is contained in:
gwen 2026-08-22 21:49:21 +02:00
parent 1771cea17e
commit f542e2182e
6 changed files with 1258 additions and 0 deletions

545
config/config.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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