mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-11-04 07:43:42 +00:00 
			
		
		
		
	add support of array expansion
This commit is contained in:
		
							
								
								
									
										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()
 | 
			
		||||
        }
 | 
			
		||||
        version = full_version(pkgbuild.epoch, pkgbuild.pkgver, pkgbuild.pkgrel)
 | 
			
		||||
        version = full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
 | 
			
		||||
 | 
			
		||||
        remote = RemoteSource(
 | 
			
		||||
            source=PackageSource.Local,
 | 
			
		||||
@ -277,7 +277,7 @@ class Package(LazyLogging):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return cls(
 | 
			
		||||
            base=pkgbuild.pkgbase,
 | 
			
		||||
            base=pkgbuild["pkgbase"],
 | 
			
		||||
            version=version,
 | 
			
		||||
            remote=remote,
 | 
			
		||||
            packages=packages,
 | 
			
		||||
@ -372,7 +372,7 @@ class Package(LazyLogging):
 | 
			
		||||
 | 
			
		||||
                yield Path(source)
 | 
			
		||||
 | 
			
		||||
        if install := pkgbuild.get("install"):
 | 
			
		||||
        if (install := pkgbuild.get("install")) is not None:
 | 
			
		||||
            yield Path(install)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@ -435,7 +435,7 @@ class Package(LazyLogging):
 | 
			
		||||
 | 
			
		||||
            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:
 | 
			
		||||
            self.logger.exception("cannot determine version of VCS package")
 | 
			
		||||
        finally:
 | 
			
		||||
 | 
			
		||||
@ -17,43 +17,20 @@
 | 
			
		||||
# 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 re
 | 
			
		||||
import shlex
 | 
			
		||||
 | 
			
		||||
from collections.abc import Generator, Iterator, Mapping
 | 
			
		||||
from collections.abc import Iterator, Mapping
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from enum import StrEnum
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Any, IO, Self
 | 
			
		||||
 | 
			
		||||
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
 | 
			
		||||
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)
 | 
			
		||||
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:
 | 
			
		||||
        fields(dict[str, PkgbuildPatch]): PKGBUILD fields
 | 
			
		||||
@ -61,11 +38,6 @@ class Pkgbuild(Mapping[str, str | list[str]]):
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    def variables(self) -> dict[str, str]:
 | 
			
		||||
        """
 | 
			
		||||
@ -106,141 +78,17 @@ class Pkgbuild(Mapping[str, str | list[str]]):
 | 
			
		||||
        Returns:
 | 
			
		||||
            Self: constructed instance of self
 | 
			
		||||
        """
 | 
			
		||||
        fields = {}
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        parser = PkgbuildParser(stream)
 | 
			
		||||
        fields = {patch.key: patch for patch in parser.parse()}
 | 
			
		||||
 | 
			
		||||
        # 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,
 | 
			
		||||
        # thus, in our terms, it is optional too
 | 
			
		||||
        if "pkgbase" not in fields:
 | 
			
		||||
            fields["pkgbase"] = fields.get("pkgname")
 | 
			
		||||
        if "pkgbase" not in fields and "pkgname" in fields:
 | 
			
		||||
            fields["pkgbase"] = fields["pkgname"]
 | 
			
		||||
 | 
			
		||||
        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]:
 | 
			
		||||
        """
 | 
			
		||||
        extract properties from internal package functions
 | 
			
		||||
@ -252,44 +100,33 @@ class Pkgbuild(Mapping[str, str | list[str]]):
 | 
			
		||||
 | 
			
		||||
        def io(package_name: str) -> IO[str]:
 | 
			
		||||
            # 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 = getattr(self, f"package_{package_name}") or self.package
 | 
			
		||||
            content = self.get(f"package_{package_name}") or self["package"]
 | 
			
		||||
            return StringIO(content)
 | 
			
		||||
 | 
			
		||||
        return {package: self.from_io(io(package)) for package in packages}
 | 
			
		||||
 | 
			
		||||
    def __getattr__(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]:
 | 
			
		||||
    def __getitem__(self, item: str) -> Any:
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
        raises an ``KeyError``.exception``
 | 
			
		||||
        fetch function with the same name
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            key(str): key name
 | 
			
		||||
            item(str): key name
 | 
			
		||||
 | 
			
		||||
        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 value is None and not key.endswith(PkgbuildToken.FunctionDeclaration):
 | 
			
		||||
            value = self.fields.get(f"{key}{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 value is None and not item.endswith(PkgbuildToken.FunctionDeclaration):
 | 
			
		||||
            value = self.fields.get(f"{item}{PkgbuildToken.FunctionDeclaration}")
 | 
			
		||||
 | 
			
		||||
        # if we still didn't find anything, we can just raise the exception
 | 
			
		||||
        if value is None:
 | 
			
		||||
            return ""
 | 
			
		||||
            raise KeyError(item)
 | 
			
		||||
 | 
			
		||||
        return value.substitute(self.variables)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -28,9 +28,9 @@ def test_package_dependencies() -> None:
 | 
			
		||||
    """
 | 
			
		||||
    must extract package dependencies
 | 
			
		||||
    """
 | 
			
		||||
    packages = dict(Versions.package_dependencies("srcinfo"))
 | 
			
		||||
    packages = dict(Versions.package_dependencies("requests"))
 | 
			
		||||
    assert packages
 | 
			
		||||
    assert packages.get("parse") is not None
 | 
			
		||||
    assert packages.get("urllib3") is not 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
									
								
							
		Reference in New Issue
	
	Block a user