feat: allow cross reference in the configuration (#131)

This commit is contained in:
2024-08-30 01:52:43 +03:00
parent 529d4caa0e
commit 9e011990ee
14 changed files with 226 additions and 36 deletions

View File

@ -120,8 +120,7 @@ class Application(ApplicationPackages, ApplicationRepository):
process_dependencies(bool): if no set, dependencies will not be processed
Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from
original package
list[Package]: updated packages list. Packager for dependencies will be copied from the original package
Examples:
In the most cases, in order to avoid build failure, it is required to add missing packages, which can be

View File

@ -236,7 +236,7 @@ class PackageArchive:
extract list of the installed packages and their content
Returns:
dict[str, FilesystemPackage]; map of package name to list of directories and files contained
dict[str, FilesystemPackage]: map of package name to list of directories and files contained
by this package
"""
result = {}

View File

@ -19,21 +19,93 @@
#
import configparser
import os
import sys
from collections.abc import Mapping, MutableMapping
from collections.abc import Generator, Mapping, MutableMapping
from string import Template
class ExtendedTemplate(Template):
"""
extension to the default :class:`Template` class, which also enabled braces regex to lookup in sections
Attributes:
braceidpattern(str): regular expression to match a colon inside braces
"""
braceidpattern = r"(?a:[_a-z0-9][_a-z0-9:]*)"
class ShellInterpolator(configparser.Interpolation):
"""
custom string interpolator, because we cannot use defaults argument due to config validation
"""
DATA_LINK_ESCAPE = "\x10"
@staticmethod
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
defaults: Mapping[str, str]) -> Generator[tuple[str, str], None, None]:
"""
extract keys and values (if available) from the configuration. In case if a key is not available, it will be
silently skipped from the result
Args:
parser(MutableMapping[str, Mapping[str, str]]): option parser
value(str): source (not-converted) value
defaults(Mapping[str, str]): default values
Yields:
tuple[str, str]: variable name used for substitution and its value
"""
def identifiers() -> Generator[tuple[str | None, str], None, None]:
# extract all found identifiers and parse them
for identifier in ExtendedTemplate(value).get_identifiers():
match identifier.split(":"):
case [lookup_option]: # single option from the same section
yield None, lookup_option
case [lookup_section, lookup_option]: # reference to another section
yield lookup_section, lookup_option
for section, option in identifiers():
# key to be substituted
key = f"{section}:{option}" if section else option
if section is not None: # foreign section case
if section not in parser:
continue # section was not found, silently skip it
values = parser[section]
else: # same section
values = defaults
if (raw := values.get(option)) is not None:
yield key, raw
@staticmethod
def environment() -> dict[str, str]:
"""
extract environment variables
Returns:
dict[str, str]: environment variables and some custom variables
"""
return os.environ | {
"prefix": sys.prefix,
}
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
defaults: Mapping[str, str]) -> str:
"""
interpolate option value
Notes:
This method is using :class:`string.Template` class in order to render both cross-references and
environment variables, because it seems that it is the most legit way to handle it. In addition,
we are using shell-like variables in some cases (see :attr:`alpm.mirror` option), thus we would like
to keep them alive.
First this method resolves substitution from the configuration and then renders environment variables
Args:
parser(MutableMapping[str, Mapping[str, str]]): option parser
section(str): section name
@ -44,8 +116,15 @@ class ShellInterpolator(configparser.Interpolation):
Returns:
str: substituted value
"""
# At the moment it seems that it is the most legit way to handle environment variables
# Template behaviour is literally the same as shell
# In addition, we are using shell-like variables in some cases (see :attr:`alpm.mirror` option),
# thus we would like to keep them alive
return Template(value).safe_substitute(os.environ)
# because any substitution effectively replace escaped $ ($$) in result, we have to escape it manually
escaped = value.replace("$$", self.DATA_LINK_ESCAPE)
# resolve internal references
variables = dict(self._extract_variables(parser, value, defaults))
internal = ExtendedTemplate(escaped).safe_substitute(variables)
# resolve enriched environment variables by using default Template class
environment = Template(internal).safe_substitute(self.environment())
# replace escaped values back
return environment.replace(self.DATA_LINK_ESCAPE, "$")

View File

@ -161,10 +161,10 @@ class Package(LazyLogging):
bool: ``True`` in case if package base looks like VCS package and ``False`` otherwise
"""
return self.base.endswith("-bzr") \
or self.base.endswith("-csv")\
or self.base.endswith("-darcs")\
or self.base.endswith("-git")\
or self.base.endswith("-hg")\
or self.base.endswith("-csv") \
or self.base.endswith("-darcs") \
or self.base.endswith("-git") \
or self.base.endswith("-hg") \
or self.base.endswith("-svn")
@property
@ -358,7 +358,7 @@ class Package(LazyLogging):
Yields:
Path: list of paths of files which belong to the package and distributed together with this tarball.
All paths are relative to the ``path``
All paths are relative to the ``path``
Raises:
PackageInfoError: if there are parsing errors
@ -504,8 +504,8 @@ class Package(LazyLogging):
timestamp(float | int): timestamp to check build date against
Returns:
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise. In case if build date
is not set by any of packages, it returns False
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
In case if build date is not set by any of packages, it returns False
"""
return any(
package.build_date > timestamp
@ -550,8 +550,8 @@ class Package(LazyLogging):
Returns:
str | None: new generated package release version if any. In case if the release contains dot (e.g. 1.2),
the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor
version equals to 1 will be appended
the minor part will be incremented by 1. If the release does not contain major.minor notation, the minor
version equals to 1 will be appended
"""
if local_version is None:
return None # local version not found, keep upstream pkgrel

View File

@ -97,7 +97,7 @@ class Waiter:
Attributes:
interval(float): interval in seconds between checks
wait_timeout(float): timeout in seconds to wait for. Negative value will result in immediate exit. Zero value
means infinite timeout
means infinite timeout
"""
wait_timeout: float