Compare commits

...

2 Commits

4 changed files with 103 additions and 47 deletions

View File

@ -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:

View File

@ -17,6 +17,7 @@
# 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 itertools
import re import re
import shlex import shlex
@ -37,6 +38,8 @@ class PkgbuildToken(StrEnum):
Attributes: Attributes:
ArrayEnds(PkgbuildToken): (class attribute) array ends token ArrayEnds(PkgbuildToken): (class attribute) array ends token
ArrayStarts(PkgbuildToken): (class attribute) array starts 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 FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token
FunctionEnds(PkgbuildToken): (class attribute) function ends token FunctionEnds(PkgbuildToken): (class attribute) function ends token
FunctionStarts(PkgbuildToken): (class attribute) function starts token FunctionStarts(PkgbuildToken): (class attribute) function starts token
@ -45,13 +48,17 @@ class PkgbuildToken(StrEnum):
ArrayStarts = "(" ArrayStarts = "("
ArrayEnds = ")" ArrayEnds = ")"
Comma = ","
Comment = "#"
FunctionDeclaration = "()" FunctionDeclaration = "()"
FunctionStarts = "{" FunctionStarts = "{"
FunctionEnds = "}" 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 simple pkgbuild reader implementation in pure python, because others sucks
@ -110,24 +117,82 @@ class Pkgbuild(Mapping[str, str | list[str]]):
parser = shlex.shlex(stream, posix=True, punctuation_chars=True) parser = shlex.shlex(stream, posix=True, punctuation_chars=True)
# ignore substitution and extend bash symbols # ignore substitution and extend bash symbols
parser.wordchars += "${}#:+" parser.wordchars += "${}#:+-@"
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes # in case of default behaviour, it will ignore, for example, segment part of url outside of quotes
parser.commenters = "" parser.commenters = ""
while token := parser.get_token(): while token := parser.get_token():
try: if (patch := cls._parse_token(token, parser)) is not None:
patch = cls._parse_token(token, parser)
fields[patch.key] = patch 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 _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
@staticmethod @staticmethod
def _parse_array(parser: shlex.shlex) -> list[str]: def _parse_array(parser: shlex.shlex) -> list[str]:
""" """
@ -147,12 +212,15 @@ class Pkgbuild(Mapping[str, str | list[str]]):
while token := parser.get_token(): while token := parser.get_token():
if token == PkgbuildToken.ArrayEnds: if token == PkgbuildToken.ArrayEnds:
break break
if token == PkgbuildToken.Comment:
parser.instream.readline()
continue
yield token yield token
if token != PkgbuildToken.ArrayEnds: if token != PkgbuildToken.ArrayEnds:
raise ValueError("No closing array bracket found") raise ValueError("No closing array bracket found")
return list(extract()) return Pkgbuild._expand_array(list(extract()))
@staticmethod @staticmethod
def _parse_function(parser: shlex.shlex) -> str: def _parse_function(parser: shlex.shlex) -> str:
@ -192,7 +260,7 @@ class Pkgbuild(Mapping[str, str | list[str]]):
return content return content
@staticmethod @staticmethod
def _parse_token(token: str, parser: shlex.shlex) -> PkgbuildPatch: def _parse_token(token: str, parser: shlex.shlex) -> PkgbuildPatch | None: # pylint: disable=too-many-return-statementste
""" """
parse single token to the PKGBUILD field parse single token to the PKGBUILD field
@ -202,9 +270,6 @@ class Pkgbuild(Mapping[str, str | list[str]]):
Returns: Returns:
PkgbuildPatch: extracted a PKGBUILD node PkgbuildPatch: extracted a PKGBUILD node
Raises:
StopIteration: if iteration reaches the end of the file
""" """
# simple assignment rule # simple assignment rule
if (match := Pkgbuild._STRING_ASSIGNMENT.match(token)) is not None: if (match := Pkgbuild._STRING_ASSIGNMENT.match(token)) is not None:
@ -212,6 +277,10 @@ class Pkgbuild(Mapping[str, str | list[str]]):
value = match.group("value") value = match.group("value")
return PkgbuildPatch(key, value) return PkgbuildPatch(key, value)
if token == PkgbuildToken.Comment:
parser.instream.readline()
return None
match parser.get_token(): match parser.get_token():
# array processing. Arrays will be sent as "key=", "(", values, ")" # array processing. Arrays will be sent as "key=", "(", values, ")"
case PkgbuildToken.ArrayStarts if (match := Pkgbuild._ARRAY_ASSIGNMENT.match(token)) is not None: case PkgbuildToken.ArrayStarts if (match := Pkgbuild._ARRAY_ASSIGNMENT.match(token)) is not None:
@ -237,9 +306,7 @@ class Pkgbuild(Mapping[str, str | list[str]]):
case other if other is not None: case other if other is not None:
return Pkgbuild._parse_token(other, parser) return Pkgbuild._parse_token(other, parser)
# reached the end of the parser return None # basically end of the parser
case None:
raise StopIteration
def packages(self) -> dict[str, Self]: def packages(self) -> dict[str, Self]:
""" """
@ -252,44 +319,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)

View File

@ -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:

View File