configitems.py
214 lines
| 6.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / configitems.py
r32983 | # configitems.py - centralized declaration of configuration option | |||
# | ||||
# Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
r33126 | import functools | |||
|
r34663 | import re | ||
r33126 | ||||
|
r51655 | from .utils import resourceutil | ||
r32984 | from . import ( | |||
|
r34236 | encoding, | ||
r32984 | error, | |||
) | ||||
|
r51655 | try: | ||
import tomllib # pytype: disable=import-error | ||||
tomllib.load # trigger lazy import | ||||
except ModuleNotFoundError: | ||||
# Python <3.11 compat | ||||
from .thirdparty import tomli as tomllib | ||||
|
r43346 | |||
r33127 | def loadconfigtable(ui, extname, configtable): | |||
"""update config item known to the ui with the extension ones""" | ||||
|
r35826 | for section, items in sorted(configtable.items()): | ||
|
r34769 | knownitems = ui._knownconfig.setdefault(section, itemregister()) | ||
r33128 | knownkeys = set(knownitems) | |||
newkeys = set(items) | ||||
for key in sorted(knownkeys & newkeys): | ||||
|
r51651 | msg = b"extension '%s' overwrites config item '%s.%s'" | ||
r33128 | msg %= (extname, section, key) | |||
|
r43347 | ui.develwarn(msg, config=b'warn-config') | ||
r33128 | ||||
knownitems.update(items) | ||||
r33127 | ||||
|
r43346 | |||
|
r49801 | class configitem: | ||
r32983 | """represent a known config item | |||
:section: the official config section where to find this item, | ||||
:name: the official name within the section, | ||||
:default: default value for this item, | ||||
|
r34663 | :alias: optional list of tuples as alternatives, | ||
:generic: this is a generic definition, match name using regular expression. | ||||
r32983 | """ | |||
|
r43346 | def __init__( | ||
self, | ||||
section, | ||||
name, | ||||
default=None, | ||||
alias=(), | ||||
generic=False, | ||||
priority=0, | ||||
experimental=False, | ||||
|
r51653 | documentation="", | ||
|
r51658 | in_core_extension=None, | ||
|
r43346 | ): | ||
r32983 | self.section = section | |||
self.name = name | ||||
self.default = default | ||||
|
r51653 | self.documentation = documentation | ||
|
r33329 | self.alias = list(alias) | ||
|
r34663 | self.generic = generic | ||
self.priority = priority | ||||
|
r43028 | self.experimental = experimental | ||
|
r34663 | self._re = None | ||
|
r51658 | self.in_core_extension = in_core_extension | ||
|
r34663 | if generic: | ||
self._re = re.compile(self.name) | ||||
|
r43346 | |||
|
r34663 | class itemregister(dict): | ||
"""A specialized dictionary that can handle wild-card selection""" | ||||
def __init__(self): | ||||
super(itemregister, self).__init__() | ||||
self._generics = set() | ||||
def update(self, other): | ||||
super(itemregister, self).update(other) | ||||
self._generics.update(other._generics) | ||||
def __setitem__(self, key, item): | ||||
super(itemregister, self).__setitem__(key, item) | ||||
if item.generic: | ||||
self._generics.add(item) | ||||
def get(self, key): | ||||
|
r34875 | baseitem = super(itemregister, self).get(key) | ||
if baseitem is not None and not baseitem.generic: | ||||
return baseitem | ||||
|
r34663 | |||
# search for a matching generic item | ||||
generics = sorted(self._generics, key=(lambda x: (x.priority, x.name))) | ||||
for item in generics: | ||||
|
r34876 | # we use 'match' instead of 'search' to make the matching simpler | ||
# for people unfamiliar with regular expression. Having the match | ||||
# rooted to the start of the string will produce less surprising | ||||
# result for user writing simple regex for sub-attribute. | ||||
# | ||||
# For example using "color\..*" match produces an unsurprising | ||||
# result, while using search could suddenly match apparently | ||||
# unrelated configuration that happens to contains "color." | ||||
# anywhere. This is a tradeoff where we favor requiring ".*" on | ||||
# some match to avoid the need to prefix most pattern with "^". | ||||
# The "^" seems more error prone. | ||||
|
r34663 | if item._re.match(key): | ||
return item | ||||
|
r34875 | return None | ||
r32984 | ||||
|
r43346 | |||
|
r51655 | def sanitize_item(item): | ||
"""Apply the transformations that are encoded on top of the pure data""" | ||||
# Set the special defaults | ||||
default_type_key = "default-type" | ||||
default_type = item.pop(default_type_key, None) | ||||
if default_type == "dynamic": | ||||
item["default"] = dynamicdefault | ||||
elif default_type == "list_type": | ||||
item["default"] = list | ||||
elif default_type == "lambda": | ||||
assert isinstance(item["default"], list) | ||||
default = [e.encode() for e in item["default"]] | ||||
item["default"] = lambda: default | ||||
elif default_type == "lazy_module": | ||||
item["default"] = lambda: encoding.encoding | ||||
else: | ||||
if default_type is not None: | ||||
msg = "invalid default config type %r for '%s.%s'" | ||||
msg %= (default_type, item["section"], item["name"]) | ||||
raise error.ProgrammingError(msg) | ||||
# config expects bytes | ||||
alias = item.get("alias") | ||||
if alias: | ||||
item["alias"] = [(k.encode(), v.encode()) for (k, v) in alias] | ||||
if isinstance(item.get("default"), str): | ||||
item["default"] = item["default"].encode() | ||||
item["section"] = item["section"].encode() | ||||
item["name"] = item["name"].encode() | ||||
def read_configitems_file(): | ||||
"""Returns the deserialized TOML structure from the configitems file""" | ||||
with resourceutil.open_resource(b"mercurial", b"configitems.toml") as fp: | ||||
return tomllib.load(fp) | ||||
def configitems_from_toml(items): | ||||
"""Register the configitems from the *deserialized* toml file""" | ||||
for item in items["items"]: | ||||
sanitize_item(item) | ||||
coreconfigitem(**item) | ||||
templates = items["templates"] | ||||
for application in items["template-applications"]: | ||||
template_items = templates[application["template"]] | ||||
for template_item in template_items: | ||||
item = template_item.copy() | ||||
prefix = application.get("prefix", "") | ||||
item["section"] = application["section"] | ||||
if prefix: | ||||
item["name"] = f'{prefix}.{item["suffix"]}' | ||||
else: | ||||
item["name"] = item["suffix"] | ||||
sanitize_item(item) | ||||
item.pop("suffix", None) | ||||
coreconfigitem(**item) | ||||
def import_configitems_from_file(): | ||||
as_toml = read_configitems_file() | ||||
configitems_from_toml(as_toml) | ||||
r32984 | coreitems = {} | |||
|
r43346 | |||
r33126 | def _register(configtable, *args, **kwargs): | |||
r32984 | item = configitem(*args, **kwargs) | |||
|
r34663 | section = configtable.setdefault(item.section, itemregister()) | ||
r32984 | if item.name in section: | |||
|
r43347 | msg = b"duplicated config item registration for '%s.%s'" | ||
r32984 | raise error.ProgrammingError(msg % (item.section, item.name)) | |||
section[item.name] = item | ||||
r32986 | ||||
|
r43346 | |||
|
r33471 | # special value for case where the default is derived from other values | ||
dynamicdefault = object() | ||||
r32986 | # Registering actual config items | |||
|
r43346 | |||
r33126 | def getitemregister(configtable): | |||
|
r34918 | f = functools.partial(_register, configtable) | ||
# export pseudo enum as configitem.* | ||||
f.dynamicdefault = dynamicdefault | ||||
return f | ||||
r33126 | ||||
|
r43346 | |||
r33126 | coreconfigitem = getitemregister(coreitems) | |||
|
r51655 | import_configitems_from_file() | ||