mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-28 01:07:18 +00:00
add support of array expansion
This commit is contained in:
parent
36a53c4262
commit
20e11cd7f4
253
src/ahriman/core/alpm/pkgbuild_parser.py
Normal file
253
src/ahriman/core/alpm/pkgbuild_parser.py
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
|
||||||
|
|
||||||
|
class PkgbuildToken(StrEnum):
|
||||||
|
"""
|
||||||
|
well-known tokens dictionary
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ArrayEnds(PkgbuildToken): (class attribute) array ends token
|
||||||
|
ArrayStarts(PkgbuildToken): (class attribute) array starts token
|
||||||
|
Comma(PkgbuildToken): (class attribute) comma token
|
||||||
|
Comment(PkgbuildToken): (class attribute) comment token
|
||||||
|
FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token
|
||||||
|
FunctionEnds(PkgbuildToken): (class attribute) function ends token
|
||||||
|
FunctionStarts(PkgbuildToken): (class attribute) function starts token
|
||||||
|
"""
|
||||||
|
|
||||||
|
ArrayStarts = "("
|
||||||
|
ArrayEnds = ")"
|
||||||
|
|
||||||
|
Comma = ","
|
||||||
|
|
||||||
|
Comment = "#"
|
||||||
|
|
||||||
|
FunctionDeclaration = "()"
|
||||||
|
FunctionStarts = "{"
|
||||||
|
FunctionEnds = "}"
|
||||||
|
|
||||||
|
|
||||||
|
class PkgbuildParser(shlex.shlex):
|
||||||
|
"""
|
||||||
|
simple pkgbuild reader implementation in pure python, because others suck
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ARRAY_ASSIGNMENT = re.compile(r"^(?P<key>\w+)=$")
|
||||||
|
# in addition to usual assignment, functions can have dash
|
||||||
|
_FUNCTION_DECLARATION = re.compile(r"^(?P<key>[\w-]+)$")
|
||||||
|
_STRING_ASSIGNMENT = re.compile(r"^(?P<key>\w+)=(?P<value>.+)$")
|
||||||
|
|
||||||
|
def __init__(self, stream: IO[str]) -> None:
|
||||||
|
"""
|
||||||
|
default constructor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stream(IO[str]): input stream containing PKGBUILD content
|
||||||
|
"""
|
||||||
|
shlex.shlex.__init__(self, stream, posix=True, punctuation_chars=True)
|
||||||
|
self._io = stream # direct access without type casting
|
||||||
|
|
||||||
|
# ignore substitution and extend bash symbols
|
||||||
|
self.wordchars += "${}#:+-@"
|
||||||
|
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes
|
||||||
|
self.commenters = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _expand_array(array: list[str]) -> list[str]:
|
||||||
|
"""
|
||||||
|
bash array expansion simulator. It takes raw parsed array and tries to expand constructions like
|
||||||
|
``(first prefix-{mid1,mid2}-suffix last)`` into ``(first, prefix-mid1-suffix prefix-mid2-suffix last)``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
array(list[str]): input array
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: either source array or expanded array if possible
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if there are errors in parser
|
||||||
|
"""
|
||||||
|
# we are using comma as marker for expansion (if any)
|
||||||
|
if PkgbuildToken.Comma not in array:
|
||||||
|
return array
|
||||||
|
# again sanity check, for expansion there are at least 3 elements (first, last and comma)
|
||||||
|
if len(array) < 3:
|
||||||
|
return array
|
||||||
|
|
||||||
|
result = []
|
||||||
|
buffer, prefix = [], None
|
||||||
|
|
||||||
|
for index, (first, second) in enumerate(itertools.pairwise(array)):
|
||||||
|
match (first, second):
|
||||||
|
# in this case we check if expansion should be started
|
||||||
|
# this condition matches "prefix{first", ","
|
||||||
|
case (_, PkgbuildToken.Comma) if PkgbuildToken.FunctionStarts in first:
|
||||||
|
prefix, part = first.rsplit(PkgbuildToken.FunctionStarts, maxsplit=1)
|
||||||
|
buffer.append(f"{prefix}{part}")
|
||||||
|
|
||||||
|
# the last element case, it matches either ",", "last}" or ",", "last}suffix"
|
||||||
|
# in case if there is suffix, it must be appended to all list elements
|
||||||
|
case (PkgbuildToken.Comma, _) if prefix is not None and PkgbuildToken.FunctionEnds in second:
|
||||||
|
part, suffix = second.rsplit(PkgbuildToken.FunctionEnds, maxsplit=1)
|
||||||
|
buffer.append(f"{prefix}{part}")
|
||||||
|
result.extend([f"{part}{suffix}" for part in buffer])
|
||||||
|
# reset state
|
||||||
|
buffer, prefix = [], None
|
||||||
|
|
||||||
|
# we have already prefix string, so we are in progress of expansion
|
||||||
|
# we always operate the last element, so this matches ",", "next"
|
||||||
|
case (PkgbuildToken.Comma, _) if prefix is not None:
|
||||||
|
buffer.append(f"{prefix}{second}")
|
||||||
|
|
||||||
|
# exactly first element of the list
|
||||||
|
case (_, _) if prefix is None and index == 0:
|
||||||
|
result.append(first)
|
||||||
|
|
||||||
|
# any next normal element
|
||||||
|
case (_, _) if prefix is None:
|
||||||
|
result.append(second)
|
||||||
|
|
||||||
|
# small sanity check
|
||||||
|
if prefix is not None:
|
||||||
|
raise ValueError(f"Could not expand `{array}` as array")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _parse_array(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
parse array from the PKGBUILD. This method will extract tokens from parser until it matches closing array,
|
||||||
|
modifying source parser state
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: extracted arrays elements
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if array is not closed
|
||||||
|
"""
|
||||||
|
def extract() -> Generator[str, None, None]:
|
||||||
|
while token := self.get_token():
|
||||||
|
if token == PkgbuildToken.ArrayEnds:
|
||||||
|
break
|
||||||
|
if token == PkgbuildToken.Comment:
|
||||||
|
self.instream.readline()
|
||||||
|
continue
|
||||||
|
yield token
|
||||||
|
|
||||||
|
if token != PkgbuildToken.ArrayEnds:
|
||||||
|
raise ValueError("No closing array bracket found")
|
||||||
|
|
||||||
|
return self._expand_array(list(extract()))
|
||||||
|
|
||||||
|
def _parse_function(self) -> str:
|
||||||
|
"""
|
||||||
|
parse function from the PKGBUILD. This method will extract tokens from parser until it matches closing function,
|
||||||
|
modifying source parser state. Instead of trying to combine tokens together, it uses positions of the file
|
||||||
|
and read content again in this range
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: function body
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if function body wasn't found or parser input stream doesn't support position reading
|
||||||
|
"""
|
||||||
|
# find start and end positions
|
||||||
|
start_position, end_position = -1, -1
|
||||||
|
while token := self.get_token():
|
||||||
|
match token:
|
||||||
|
case PkgbuildToken.FunctionStarts:
|
||||||
|
start_position = self._io.tell() - 1
|
||||||
|
case PkgbuildToken.FunctionEnds:
|
||||||
|
end_position = self._io.tell()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not 0 < start_position < end_position:
|
||||||
|
raise ValueError("Function body wasn't found")
|
||||||
|
|
||||||
|
# read the specified interval from source stream
|
||||||
|
self._io.seek(start_position - 1) # start from the previous symbol
|
||||||
|
content = self._io.read(end_position - start_position)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:
|
||||||
|
"""
|
||||||
|
parse single token to the PKGBUILD field
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token(str): current token
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
PkgbuildPatch: extracted a PKGBUILD node
|
||||||
|
"""
|
||||||
|
# simple assignment rule
|
||||||
|
if (match := self._STRING_ASSIGNMENT.match(token)) is not None:
|
||||||
|
key = match.group("key")
|
||||||
|
value = match.group("value")
|
||||||
|
yield PkgbuildPatch(key, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if token == PkgbuildToken.Comment:
|
||||||
|
self.instream.readline()
|
||||||
|
return
|
||||||
|
|
||||||
|
match self.get_token():
|
||||||
|
# array processing. Arrays will be sent as "key=", "(", values, ")"
|
||||||
|
case PkgbuildToken.ArrayStarts if (match := self._ARRAY_ASSIGNMENT.match(token)) is not None:
|
||||||
|
key = match.group("key")
|
||||||
|
value = self._parse_array()
|
||||||
|
yield PkgbuildPatch(key, value)
|
||||||
|
|
||||||
|
# functions processing. Function will be sent as "name", "()", "{", body, "}"
|
||||||
|
case PkgbuildToken.FunctionDeclaration if self._FUNCTION_DECLARATION.match(token):
|
||||||
|
key = f"{token}{PkgbuildToken.FunctionDeclaration}"
|
||||||
|
value = self._parse_function()
|
||||||
|
yield PkgbuildPatch(key, value) # this is not mistake, assign to token without ()
|
||||||
|
|
||||||
|
# special function case, where "(" and ")" are separated tokens, e.g. "pkgver ( )"
|
||||||
|
case PkgbuildToken.ArrayStarts if self._FUNCTION_DECLARATION.match(token):
|
||||||
|
next_token = self.get_token()
|
||||||
|
if next_token == PkgbuildToken.ArrayEnds: # replace closing bracket with "()"
|
||||||
|
next_token = PkgbuildToken.FunctionDeclaration
|
||||||
|
self.push_token(next_token) # type: ignore[arg-type]
|
||||||
|
yield from self._parse_token(token)
|
||||||
|
|
||||||
|
# some random token received without continuation, lets guess it is empty assignment (i.e. key=)
|
||||||
|
case other if other is not None:
|
||||||
|
yield from self._parse_token(other)
|
||||||
|
|
||||||
|
def parse(self) -> Generator[PkgbuildPatch, None, None]:
|
||||||
|
"""
|
||||||
|
parse source stream and yield parsed entries
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
PkgbuildPatch: extracted a PKGBUILD node
|
||||||
|
"""
|
||||||
|
for token in self:
|
||||||
|
yield from self._parse_token(token)
|
@ -266,7 +266,7 @@ class Package(LazyLogging):
|
|||||||
)
|
)
|
||||||
for package, properties in pkgbuild.packages().items()
|
for package, properties in pkgbuild.packages().items()
|
||||||
}
|
}
|
||||||
version = full_version(pkgbuild.epoch, pkgbuild.pkgver, pkgbuild.pkgrel)
|
version = full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||||
|
|
||||||
remote = RemoteSource(
|
remote = RemoteSource(
|
||||||
source=PackageSource.Local,
|
source=PackageSource.Local,
|
||||||
@ -277,7 +277,7 @@ class Package(LazyLogging):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
base=pkgbuild.pkgbase,
|
base=pkgbuild["pkgbase"],
|
||||||
version=version,
|
version=version,
|
||||||
remote=remote,
|
remote=remote,
|
||||||
packages=packages,
|
packages=packages,
|
||||||
@ -372,7 +372,7 @@ class Package(LazyLogging):
|
|||||||
|
|
||||||
yield Path(source)
|
yield Path(source)
|
||||||
|
|
||||||
if install := pkgbuild.get("install"):
|
if (install := pkgbuild.get("install")) is not None:
|
||||||
yield Path(install)
|
yield Path(install)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -435,7 +435,7 @@ class Package(LazyLogging):
|
|||||||
|
|
||||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
||||||
|
|
||||||
return full_version(pkgbuild.epoch, pkgbuild.pkgver, pkgbuild.pkgrel)
|
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("cannot determine version of VCS package")
|
self.logger.exception("cannot determine version of VCS package")
|
||||||
finally:
|
finally:
|
||||||
|
@ -17,43 +17,20 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import re
|
from collections.abc import Iterator, Mapping
|
||||||
import shlex
|
|
||||||
|
|
||||||
from collections.abc import Generator, Iterator, Mapping
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, IO, Self
|
from typing import Any, IO, Self
|
||||||
|
|
||||||
|
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
|
||||||
|
|
||||||
class PkgbuildToken(StrEnum):
|
|
||||||
"""
|
|
||||||
well-known tokens dictionary
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
ArrayEnds(PkgbuildToken): (class attribute) array ends token
|
|
||||||
ArrayStarts(PkgbuildToken): (class attribute) array starts token
|
|
||||||
FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token
|
|
||||||
FunctionEnds(PkgbuildToken): (class attribute) function ends token
|
|
||||||
FunctionStarts(PkgbuildToken): (class attribute) function starts token
|
|
||||||
"""
|
|
||||||
|
|
||||||
ArrayStarts = "("
|
|
||||||
ArrayEnds = ")"
|
|
||||||
|
|
||||||
FunctionDeclaration = "()"
|
|
||||||
FunctionStarts = "{"
|
|
||||||
FunctionEnds = "}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Pkgbuild(Mapping[str, str | list[str]]):
|
class Pkgbuild(Mapping[str, Any]):
|
||||||
"""
|
"""
|
||||||
simple pkgbuild reader implementation in pure python, because others sucks
|
model and proxy for PKGBUILD properties
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
fields(dict[str, PkgbuildPatch]): PKGBUILD fields
|
fields(dict[str, PkgbuildPatch]): PKGBUILD fields
|
||||||
@ -61,11 +38,6 @@ class Pkgbuild(Mapping[str, str | list[str]]):
|
|||||||
|
|
||||||
fields: dict[str, PkgbuildPatch]
|
fields: dict[str, PkgbuildPatch]
|
||||||
|
|
||||||
_ARRAY_ASSIGNMENT = re.compile(r"^(?P<key>\w+)=$")
|
|
||||||
_STRING_ASSIGNMENT = re.compile(r"^(?P<key>\w+)=(?P<value>.+)$")
|
|
||||||
# in addition, functions can have dash to usual assignment
|
|
||||||
_FUNCTION_DECLARATION = re.compile(r"^(?P<key>[\w-]+)$")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def variables(self) -> dict[str, str]:
|
def variables(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@ -106,141 +78,17 @@ class Pkgbuild(Mapping[str, str | list[str]]):
|
|||||||
Returns:
|
Returns:
|
||||||
Self: constructed instance of self
|
Self: constructed instance of self
|
||||||
"""
|
"""
|
||||||
fields = {}
|
parser = PkgbuildParser(stream)
|
||||||
|
fields = {patch.key: patch for patch in parser.parse()}
|
||||||
parser = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
||||||
# ignore substitution and extend bash symbols
|
|
||||||
parser.wordchars += "${}#:+"
|
|
||||||
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes
|
|
||||||
parser.commenters = ""
|
|
||||||
while token := parser.get_token():
|
|
||||||
try:
|
|
||||||
patch = cls._parse_token(token, parser)
|
|
||||||
fields[patch.key] = patch
|
|
||||||
except StopIteration:
|
|
||||||
break
|
|
||||||
|
|
||||||
# pkgbase is optional field, the pkgname must be used instead if not set
|
# pkgbase is optional field, the pkgname must be used instead if not set
|
||||||
# however, pkgname is not presented is "package()" functions which we are parsing here too,
|
# however, pkgname is not presented is "package()" functions which we are parsing here too,
|
||||||
# thus, in our terms, it is optional too
|
# thus, in our terms, it is optional too
|
||||||
if "pkgbase" not in fields:
|
if "pkgbase" not in fields and "pkgname" in fields:
|
||||||
fields["pkgbase"] = fields.get("pkgname")
|
fields["pkgbase"] = fields["pkgname"]
|
||||||
|
|
||||||
return cls({key: value for key, value in fields.items() if key})
|
return cls({key: value for key, value in fields.items() if key})
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_array(parser: shlex.shlex) -> list[str]:
|
|
||||||
"""
|
|
||||||
parse array from the PKGBUILD. This method will extract tokens from parser until it matches closing array,
|
|
||||||
modifying source parser state
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parser(shlex.shlex): shell parser instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[str]: extracted arrays elements
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: if array is not closed
|
|
||||||
"""
|
|
||||||
def extract() -> Generator[str, None, None]:
|
|
||||||
while token := parser.get_token():
|
|
||||||
if token == PkgbuildToken.ArrayEnds:
|
|
||||||
break
|
|
||||||
yield token
|
|
||||||
|
|
||||||
if token != PkgbuildToken.ArrayEnds:
|
|
||||||
raise ValueError("No closing array bracket found")
|
|
||||||
|
|
||||||
return list(extract())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_function(parser: shlex.shlex) -> str:
|
|
||||||
"""
|
|
||||||
parse function from the PKGBUILD. This method will extract tokens from parser until it matches closing function,
|
|
||||||
modifying source parser state. Instead of trying to combine tokens together, it uses positions of the file
|
|
||||||
and read content again in this range
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parser(shlex.shlex): shell parser instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: function body
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: if function body wasn't found or parser input stream doesn't support position reading
|
|
||||||
"""
|
|
||||||
io: IO[str] = parser.instream # type: ignore[assignment]
|
|
||||||
|
|
||||||
# find start and end positions
|
|
||||||
start_position, end_position = -1, -1
|
|
||||||
while token := parser.get_token():
|
|
||||||
match token:
|
|
||||||
case PkgbuildToken.FunctionStarts:
|
|
||||||
start_position = io.tell() - 1
|
|
||||||
case PkgbuildToken.FunctionEnds:
|
|
||||||
end_position = io.tell()
|
|
||||||
break
|
|
||||||
|
|
||||||
if not 0 < start_position < end_position:
|
|
||||||
raise ValueError("Function body wasn't found")
|
|
||||||
|
|
||||||
# read the specified interval from source stream
|
|
||||||
io.seek(start_position - 1) # start from the previous symbol
|
|
||||||
content = io.read(end_position - start_position)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_token(token: str, parser: shlex.shlex) -> PkgbuildPatch:
|
|
||||||
"""
|
|
||||||
parse single token to the PKGBUILD field
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token(str): current token
|
|
||||||
parser(shlex.shlex): shell parser instance
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PkgbuildPatch: extracted a PKGBUILD node
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
StopIteration: if iteration reaches the end of the file
|
|
||||||
"""
|
|
||||||
# simple assignment rule
|
|
||||||
if (match := Pkgbuild._STRING_ASSIGNMENT.match(token)) is not None:
|
|
||||||
key = match.group("key")
|
|
||||||
value = match.group("value")
|
|
||||||
return PkgbuildPatch(key, value)
|
|
||||||
|
|
||||||
match parser.get_token():
|
|
||||||
# array processing. Arrays will be sent as "key=", "(", values, ")"
|
|
||||||
case PkgbuildToken.ArrayStarts if (match := Pkgbuild._ARRAY_ASSIGNMENT.match(token)) is not None:
|
|
||||||
key = match.group("key")
|
|
||||||
value = Pkgbuild._parse_array(parser)
|
|
||||||
return PkgbuildPatch(key, value)
|
|
||||||
|
|
||||||
# functions processing. Function will be sent as "name", "()", "{", body, "}"
|
|
||||||
case PkgbuildToken.FunctionDeclaration if Pkgbuild._FUNCTION_DECLARATION.match(token):
|
|
||||||
key = f"{token}{PkgbuildToken.FunctionDeclaration}"
|
|
||||||
value = Pkgbuild._parse_function(parser)
|
|
||||||
return PkgbuildPatch(key, value) # this is not mistake, assign to token without ()
|
|
||||||
|
|
||||||
# special function case, where "(" and ")" are separated tokens, e.g. "pkgver ( )"
|
|
||||||
case PkgbuildToken.ArrayStarts if Pkgbuild._FUNCTION_DECLARATION.match(token):
|
|
||||||
next_token = parser.get_token()
|
|
||||||
if next_token == PkgbuildToken.ArrayEnds: # replace closing bracket with "()"
|
|
||||||
next_token = PkgbuildToken.FunctionDeclaration
|
|
||||||
parser.push_token(next_token) # type: ignore[arg-type]
|
|
||||||
return Pkgbuild._parse_token(token, parser)
|
|
||||||
|
|
||||||
# some random token received without continuation, lets guess it is empty assignment (i.e. key=)
|
|
||||||
case other if other is not None:
|
|
||||||
return Pkgbuild._parse_token(other, parser)
|
|
||||||
|
|
||||||
# reached the end of the parser
|
|
||||||
case None:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
def packages(self) -> dict[str, Self]:
|
def packages(self) -> dict[str, Self]:
|
||||||
"""
|
"""
|
||||||
extract properties from internal package functions
|
extract properties from internal package functions
|
||||||
@ -252,44 +100,33 @@ class Pkgbuild(Mapping[str, str | list[str]]):
|
|||||||
|
|
||||||
def io(package_name: str) -> IO[str]:
|
def io(package_name: str) -> IO[str]:
|
||||||
# try to read package specific function and fallback to default otherwise
|
# try to read package specific function and fallback to default otherwise
|
||||||
# content = self.get_as(f"package_{package_name}") or self.get_as("package")
|
content = self.get(f"package_{package_name}") or self["package"]
|
||||||
content = getattr(self, f"package_{package_name}") or self.package
|
|
||||||
return StringIO(content)
|
return StringIO(content)
|
||||||
|
|
||||||
return {package: self.from_io(io(package)) for package in packages}
|
return {package: self.from_io(io(package)) for package in packages}
|
||||||
|
|
||||||
def __getattr__(self, item: str) -> Any:
|
def __getitem__(self, item: str) -> Any:
|
||||||
"""
|
|
||||||
proxy method for PKGBUILD properties
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item(str): property name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Any: attribute by its name
|
|
||||||
"""
|
|
||||||
return self[item]
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> str | list[str]:
|
|
||||||
"""
|
"""
|
||||||
get the field of the PKGBUILD. This method tries to get exact key value if possible; if none found, it tries to
|
get the field of the PKGBUILD. This method tries to get exact key value if possible; if none found, it tries to
|
||||||
fetch function with the same name. And, finally, it returns empty value if nothing found, so this function never
|
fetch function with the same name
|
||||||
raises an ``KeyError``.exception``
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key(str): key name
|
item(str): key name
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | list[str]: value by the key
|
Any: substituted value by the key
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: if key doesn't exist
|
||||||
"""
|
"""
|
||||||
value = self.fields.get(key)
|
value = self.fields.get(item)
|
||||||
# if the key wasn't found and user didn't ask for function explicitly, we can try to get by function name
|
# if the key wasn't found and user didn't ask for function explicitly, we can try to get by function name
|
||||||
if value is None and not key.endswith(PkgbuildToken.FunctionDeclaration):
|
if value is None and not item.endswith(PkgbuildToken.FunctionDeclaration):
|
||||||
value = self.fields.get(f"{key}{PkgbuildToken.FunctionDeclaration}")
|
value = self.fields.get(f"{item}{PkgbuildToken.FunctionDeclaration}")
|
||||||
# if we still didn't find anything, we fall back to empty value (just like shell)
|
|
||||||
# to avoid recursion here, we can just drop from the method
|
# if we still didn't find anything, we can just raise the exception
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
raise KeyError(item)
|
||||||
|
|
||||||
return value.substitute(self.variables)
|
return value.substitute(self.variables)
|
||||||
|
|
||||||
|
@ -28,9 +28,9 @@ def test_package_dependencies() -> None:
|
|||||||
"""
|
"""
|
||||||
must extract package dependencies
|
must extract package dependencies
|
||||||
"""
|
"""
|
||||||
packages = dict(Versions.package_dependencies("srcinfo"))
|
packages = dict(Versions.package_dependencies("requests"))
|
||||||
assert packages
|
assert packages
|
||||||
assert packages.get("parse") is not None
|
assert packages.get("urllib3") is not None
|
||||||
|
|
||||||
|
|
||||||
def test_package_dependencies_missing() -> None:
|
def test_package_dependencies_missing() -> None:
|
||||||
|
0
tests/ahriman/core/alpm/test_pkgbuild_parser.py
Normal file
0
tests/ahriman/core/alpm/test_pkgbuild_parser.py
Normal file
0
tests/ahriman/models/test_pkgbuild.py
Normal file
0
tests/ahriman/models/test_pkgbuild.py
Normal file
Loading…
Reference in New Issue
Block a user