mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-11-04 15:53:41 +00:00 
			
		
		
		
	pkgbuild parser impl
This commit is contained in:
		@ -27,7 +27,7 @@ import re
 | 
				
			|||||||
import selectors
 | 
					import selectors
 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from collections.abc import Callable, Generator, Iterable
 | 
					from collections.abc import Callable, Generator, Iterable, Mapping
 | 
				
			||||||
from dataclasses import asdict
 | 
					from dataclasses import asdict
 | 
				
			||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
@ -407,7 +407,7 @@ def safe_filename(source: str) -> str:
 | 
				
			|||||||
    return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source)
 | 
					    return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *,
 | 
					def srcinfo_property(key: str, srcinfo: Mapping[str, Any], package_srcinfo: Mapping[str, Any], *,
 | 
				
			||||||
                     default: Any = None) -> Any:
 | 
					                     default: Any = None) -> Any:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    extract property from SRCINFO. This method extracts property from package if this property is presented in
 | 
					    extract property from SRCINFO. This method extracts property from package if this property is presented in
 | 
				
			||||||
@ -416,8 +416,8 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        key(str): key to extract
 | 
					        key(str): key to extract
 | 
				
			||||||
        srcinfo(dict[str, Any]): root structure of SRCINFO
 | 
					        srcinfo(Mapping[str, Any]): root structure of SRCINFO
 | 
				
			||||||
        package_srcinfo(dict[str, Any]): package specific SRCINFO
 | 
					        package_srcinfo(Mapping[str, Any]): package specific SRCINFO
 | 
				
			||||||
        default(Any, optional): the default value for the specified key (Default value = None)
 | 
					        default(Any, optional): the default value for the specified key (Default value = None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns:
 | 
					    Returns:
 | 
				
			||||||
@ -426,7 +426,7 @@ def srcinfo_property(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[st
 | 
				
			|||||||
    return package_srcinfo.get(key) or srcinfo.get(key) or default
 | 
					    return package_srcinfo.get(key) or srcinfo.get(key) or default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: dict[str, Any], *,
 | 
					def srcinfo_property_list(key: str, srcinfo: Mapping[str, Any], package_srcinfo: Mapping[str, Any], *,
 | 
				
			||||||
                          architecture: str | None = None) -> list[Any]:
 | 
					                          architecture: str | None = None) -> list[Any]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    extract list property from SRCINFO. Unlike :func:`srcinfo_property()` it supposes that default return value is
 | 
					    extract list property from SRCINFO. Unlike :func:`srcinfo_property()` it supposes that default return value is
 | 
				
			||||||
@ -435,8 +435,8 @@ def srcinfo_property_list(key: str, srcinfo: dict[str, Any], package_srcinfo: di
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Args:
 | 
					    Args:
 | 
				
			||||||
        key(str): key to extract
 | 
					        key(str): key to extract
 | 
				
			||||||
        srcinfo(dict[str, Any]): root structure of SRCINFO
 | 
					        srcinfo(Mapping[str, Any]): root structure of SRCINFO
 | 
				
			||||||
        package_srcinfo(dict[str, Any]): package specific SRCINFO
 | 
					        package_srcinfo(Mapping[str, Any]): package specific SRCINFO
 | 
				
			||||||
        architecture(str | None, optional): package architecture if set (Default value = None)
 | 
					        architecture(str | None, optional): package architecture if set (Default value = None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns:
 | 
					    Returns:
 | 
				
			||||||
 | 
				
			|||||||
@ -37,6 +37,7 @@ from ahriman.core.log import LazyLogging
 | 
				
			|||||||
from ahriman.core.utils import check_output, dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow
 | 
					from ahriman.core.utils import check_output, dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow
 | 
				
			||||||
from ahriman.models.package_description import PackageDescription
 | 
					from ahriman.models.package_description import PackageDescription
 | 
				
			||||||
from ahriman.models.package_source import PackageSource
 | 
					from ahriman.models.package_source import PackageSource
 | 
				
			||||||
 | 
					from ahriman.models.pkgbuild import Pkgbuild
 | 
				
			||||||
from ahriman.models.remote_source import RemoteSource
 | 
					from ahriman.models.remote_source import RemoteSource
 | 
				
			||||||
from ahriman.models.repository_paths import RepositoryPaths
 | 
					from ahriman.models.repository_paths import RepositoryPaths
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -255,25 +256,23 @@ class Package(LazyLogging):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
            Self: package properties
 | 
					            Self: package properties
 | 
				
			||||||
 | 
					 | 
				
			||||||
        Raises:
 | 
					 | 
				
			||||||
            PackageInfoError: if there are parsing errors
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
 | 
					        pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
 | 
				
			||||||
        srcinfo, errors = parse_srcinfo(srcinfo_source)
 | 
					 | 
				
			||||||
        if errors:
 | 
					 | 
				
			||||||
            raise PackageInfoError(errors)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        packages = {
 | 
					        packages = {
 | 
				
			||||||
            package: PackageDescription(
 | 
					            package: PackageDescription(
 | 
				
			||||||
                depends=srcinfo_property_list("depends", srcinfo, properties, architecture=architecture),
 | 
					                depends=srcinfo_property_list("depends", pkgbuild, properties, architecture=architecture),
 | 
				
			||||||
                make_depends=srcinfo_property_list("makedepends", srcinfo, properties, architecture=architecture),
 | 
					                make_depends=srcinfo_property_list("makedepends", pkgbuild, properties, architecture=architecture),
 | 
				
			||||||
                opt_depends=srcinfo_property_list("optdepends", srcinfo, properties, architecture=architecture),
 | 
					                opt_depends=srcinfo_property_list("optdepends", pkgbuild, properties, architecture=architecture),
 | 
				
			||||||
                check_depends=srcinfo_property_list("checkdepends", srcinfo, properties, architecture=architecture),
 | 
					                check_depends=srcinfo_property_list("checkdepends", pkgbuild, properties, architecture=architecture),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            for package, properties in srcinfo["packages"].items()
 | 
					            for package, properties in pkgbuild.packages().items()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
 | 
					        version = full_version(
 | 
				
			||||||
 | 
					            pkgbuild.get_as("epoch", str, default=None),
 | 
				
			||||||
 | 
					            pkgbuild.get_as("pkgver", str),
 | 
				
			||||||
 | 
					            pkgbuild.get_as("pkgrel", str),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        remote = RemoteSource(
 | 
					        remote = RemoteSource(
 | 
				
			||||||
            source=PackageSource.Local,
 | 
					            source=PackageSource.Local,
 | 
				
			||||||
@ -284,7 +283,7 @@ class Package(LazyLogging):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return cls(
 | 
					        return cls(
 | 
				
			||||||
            base=srcinfo["pkgbase"],
 | 
					            base=pkgbuild.get_as("pkgbase", str),
 | 
				
			||||||
            version=version,
 | 
					            version=version,
 | 
				
			||||||
            remote=remote,
 | 
					            remote=remote,
 | 
				
			||||||
            packages=packages,
 | 
					            packages=packages,
 | 
				
			||||||
@ -363,16 +362,12 @@ class Package(LazyLogging):
 | 
				
			|||||||
        Raises:
 | 
					        Raises:
 | 
				
			||||||
            PackageInfoError: if there are parsing errors
 | 
					            PackageInfoError: if there are parsing errors
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
 | 
					        pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
 | 
				
			||||||
        srcinfo, errors = parse_srcinfo(srcinfo_source)
 | 
					 | 
				
			||||||
        if errors:
 | 
					 | 
				
			||||||
            raise PackageInfoError(errors)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # we could use arch property, but for consistency it is better to call special method
 | 
					        # we could use arch property, but for consistency it is better to call special method
 | 
				
			||||||
        architectures = Package.supported_architectures(path)
 | 
					        architectures = Package.supported_architectures(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for architecture in architectures:
 | 
					        for architecture in architectures:
 | 
				
			||||||
            for source in srcinfo_property_list("source", srcinfo, {}, architecture=architecture):
 | 
					            for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture):
 | 
				
			||||||
                if "::" in source:
 | 
					                if "::" in source:
 | 
				
			||||||
                    _, source = source.split("::", 1)  # in case if filename is specified, remove it
 | 
					                    _, source = source.split("::", 1)  # in case if filename is specified, remove it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -383,7 +378,7 @@ class Package(LazyLogging):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                yield Path(source)
 | 
					                yield Path(source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (install := srcinfo.get("install", None)) is not None:
 | 
					        if isinstance(install := pkgbuild.get("install"), str):  # well, in reality it is either None or str
 | 
				
			||||||
            yield Path(install)
 | 
					            yield Path(install)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
@ -396,15 +391,9 @@ class Package(LazyLogging):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Returns:
 | 
					        Returns:
 | 
				
			||||||
            set[str]: list of package supported architectures
 | 
					            set[str]: list of package supported architectures
 | 
				
			||||||
 | 
					 | 
				
			||||||
        Raises:
 | 
					 | 
				
			||||||
            PackageInfoError: if there are parsing errors
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        srcinfo_source = check_output("makepkg", "--printsrcinfo", cwd=path)
 | 
					        pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
 | 
				
			||||||
        srcinfo, errors = parse_srcinfo(srcinfo_source)
 | 
					        return set(pkgbuild.get("arch", []))
 | 
				
			||||||
        if errors:
 | 
					 | 
				
			||||||
            raise PackageInfoError(errors)
 | 
					 | 
				
			||||||
        return set(srcinfo.get("arch", []))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]:
 | 
					    def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										298
									
								
								src/ahriman/models/pkgbuild.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								src/ahriman/models/pkgbuild.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,298 @@
 | 
				
			|||||||
 | 
					#
 | 
				
			||||||
 | 
					# 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 re
 | 
				
			||||||
 | 
					import shlex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from collections.abc import Generator, Iterator, Mapping
 | 
				
			||||||
 | 
					from dataclasses import dataclass
 | 
				
			||||||
 | 
					from enum import StrEnum
 | 
				
			||||||
 | 
					from io import StringIO
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from typing import IO, Self, TypeVar, cast
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ahriman.models.pkgbuild_patch import PkgbuildPatch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					T = TypeVar("T", str, list[str])
 | 
				
			||||||
 | 
					U = TypeVar("U", str, list[str], None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PkgbuildToken(StrEnum):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    well-known tokens dictionary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Attributes:
 | 
				
			||||||
 | 
					        ArrayStarts(PkgbuildToken): (class attribute) array starts token
 | 
				
			||||||
 | 
					        ArrayEnds(PkgbuildToken): (class attribute) array ends token
 | 
				
			||||||
 | 
					        FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token
 | 
				
			||||||
 | 
					        FunctionStarts(PkgbuildToken): (class attribute) function starts token
 | 
				
			||||||
 | 
					        FunctionEnds(PkgbuildToken): (class attribute) function ends token
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ArrayStarts = "("
 | 
				
			||||||
 | 
					    ArrayEnds = ")"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FunctionDeclaration = "()"
 | 
				
			||||||
 | 
					    FunctionStarts = "{"
 | 
				
			||||||
 | 
					    FunctionEnds = "}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dataclass(frozen=True)
 | 
				
			||||||
 | 
					class Pkgbuild(Mapping[str, str | list[str]]):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    simple pkgbuild reader implementation in pure python, because others sucks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Attributes:
 | 
				
			||||||
 | 
					        fields(dict[str, PkgbuildPatch]): PKGBUILD fields
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fields: dict[str, PkgbuildPatch]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _ARRAY_ASSIGNMENT_REGEX = re.compile(r"^(?P<key>\w+)=$")
 | 
				
			||||||
 | 
					    _STRING_ASSIGNMENT_REGEX = re.compile(r"^(?P<key>\w+)=(?P<value>.+)$")
 | 
				
			||||||
 | 
					    # in addition functions can have dash to usual assignment
 | 
				
			||||||
 | 
					    _FUNCTION_DECLARATION_REGEX = re.compile(r"^(?P<key>[\w-]+)$")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def variables(self) -> dict[str, str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        list of variables defined and (maybe) used in this PKGBUILD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            dict[str, str]: map of variable name to its value. The value will be included here in case if it presented
 | 
				
			||||||
 | 
					            in the internal dictionary, it is not a function and the value has string type
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            key: value.value
 | 
				
			||||||
 | 
					            for key, value in self.fields.items()
 | 
				
			||||||
 | 
					            if not value.is_function and isinstance(value.value, str)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def from_file(cls, path: Path) -> Self:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        parse PKGBUILD from the file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            path(Path): path to the PKGBUILD file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Self: constructed instance of self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with path.open() as input_file:
 | 
				
			||||||
 | 
					            return cls.from_io(input_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def from_io(cls, stream: IO[str]) -> Self:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        parse PKGBUILD from input stream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            stream: IO[str]: input stream containing PKGBUILD content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Self: constructed instance of self
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        fields = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        parser = shlex.shlex(stream, posix=True, punctuation_chars=True)
 | 
				
			||||||
 | 
					        while token := parser.get_token():
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                key, value = cls._parse_token(token, parser)
 | 
				
			||||||
 | 
					                fields[key] = value
 | 
				
			||||||
 | 
					            except StopIteration:
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cls(fields)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @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()
 | 
				
			||||||
 | 
					                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 + 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _parse_token(token: str, parser: shlex.shlex) -> tuple[str, PkgbuildPatch]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        parse single token to the PKGBUILD field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            token(str): current token
 | 
				
			||||||
 | 
					            parser(shlex.shlex): shell parser instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            tuple[str, PkgbuildPatch]: extracted a pair of key and its value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Raises:
 | 
				
			||||||
 | 
					            StopIteration: if iteration reaches the end of the file'
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # simple assignment rule
 | 
				
			||||||
 | 
					        if (match := Pkgbuild._STRING_ASSIGNMENT_REGEX.match(token)) is not None:
 | 
				
			||||||
 | 
					            key = match.group("key")
 | 
				
			||||||
 | 
					            value = match.group("value")
 | 
				
			||||||
 | 
					            return key, PkgbuildPatch(key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match parser.get_token():
 | 
				
			||||||
 | 
					            # array processing. Arrays will be sent as "key=", "(", values, ")"
 | 
				
			||||||
 | 
					            case PkgbuildToken.ArrayStarts if (match := Pkgbuild._ARRAY_ASSIGNMENT_REGEX.match(token)) is not None:
 | 
				
			||||||
 | 
					                key = match.group("key")
 | 
				
			||||||
 | 
					                value = Pkgbuild._parse_array(parser)
 | 
				
			||||||
 | 
					                return key, PkgbuildPatch(key, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # functions processing. Function will be sent as "name", "()", "{", body, "}"
 | 
				
			||||||
 | 
					            case PkgbuildToken.FunctionDeclaration if Pkgbuild._FUNCTION_DECLARATION_REGEX.match(token):
 | 
				
			||||||
 | 
					                key = f"{token}{PkgbuildToken.FunctionDeclaration}"
 | 
				
			||||||
 | 
					                value = Pkgbuild._parse_function(parser)
 | 
				
			||||||
 | 
					                return token, PkgbuildPatch(key, value)  # this is not mistake, assign to token without ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # 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 get_as(self, key: str, return_type: type[T], **kwargs: T | U) -> T | U:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        type guard for getting value by key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            key(str): key name
 | 
				
			||||||
 | 
					            return_type(type[T]): return type, either ``str`` or ``list[str]``
 | 
				
			||||||
 | 
					            default(U): default value to return if no key found
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            T | U: value associated with key or default value if no value found and fallback is provided
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Raises:
 | 
				
			||||||
 | 
					            KeyError: if no key found and no default has been provided
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        del return_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if key not in self:
 | 
				
			||||||
 | 
					            if "default" in kwargs:
 | 
				
			||||||
 | 
					                return kwargs["default"]
 | 
				
			||||||
 | 
					            raise KeyError(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return cast(T, self[key])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def packages(self) -> dict[str, Self]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        extract properties from internal package functions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            dict[str, Self]: map of package name to its inner properties if defined
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        packages = [self["pkgname"]] if isinstance(self["pkgname"], str) else self["pkgname"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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}", str, default=None) or self.get_as("package", str)
 | 
				
			||||||
 | 
					            return StringIO(content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {package: self.from_io(io(package)) for package in packages}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __getitem__(self, key: str) -> str | list[str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        get the field of the PKGBUILD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            key(str): key name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            str | list[str]: value by the key
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.fields[key].substitute(self.variables)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __iter__(self) -> Iterator[str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        iterate over the fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Iterator[str]: keys iterator
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return iter(self.fields)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __len__(self) -> int:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        get length of the mapping
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            int: amount of the fields in this PKGBUILD
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return len(self.fields)
 | 
				
			||||||
@ -21,6 +21,7 @@ import shlex
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass, fields
 | 
					from dataclasses import dataclass, fields
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from string import Template
 | 
				
			||||||
from typing import Any, Generator, Self
 | 
					from typing import Any, Generator, Self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ahriman.core.utils import dataclass_view, filter_json
 | 
					from ahriman.core.utils import dataclass_view, filter_json
 | 
				
			||||||
@ -167,6 +168,20 @@ class PkgbuildPatch:
 | 
				
			|||||||
            return f"{self.key} {self.value}"  # no quoting enabled here
 | 
					            return f"{self.key} {self.value}"  # no quoting enabled here
 | 
				
			||||||
        return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
 | 
					        return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def substitute(self, variables: dict[str, str]) -> str | list[str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        substitute variables into the value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            variables(dict[str, str]): map of variables available for usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            str | list[str]: substituted value. All unknown variables will remain the same
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if isinstance(self.value, str):
 | 
				
			||||||
 | 
					            return Template(self.value).safe_substitute(variables)
 | 
				
			||||||
 | 
					        return [Template(value).safe_substitute(variables) for value in self.value]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def view(self) -> dict[str, Any]:
 | 
					    def view(self) -> dict[str, Any]:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        generate json patch view
 | 
					        generate json patch view
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user