mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-30 21:33:43 +00:00 
			
		
		
		
	fix: parse array variable from command
This commit is contained in:
		
							
								
								
									
										12
									
								
								docs/faq.rst
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								docs/faq.rst
									
									
									
									
									
								
							| @ -208,6 +208,18 @@ This command will prompt for new value of the PKGBUILD variable ``version``. You | |||||||
|  |  | ||||||
|    sudo -u ahriman ahriman patch-add ahriman version version.patch |    sudo -u ahriman ahriman patch-add ahriman version version.patch | ||||||
|  |  | ||||||
|  | The command also supports arrays, but in this case you need to specify full array, e.g. | ||||||
|  |  | ||||||
|  | .. code-block:: shell | ||||||
|  |  | ||||||
|  |    sudo -u ahriman ahriman patch-add ahriman depends | ||||||
|  |  | ||||||
|  |    Post new function or variable value below. Press Ctrl-D to finish: | ||||||
|  |    (python python-aiohttp) | ||||||
|  |    ^D | ||||||
|  |  | ||||||
|  | will set depends PKGBUILD variable (exactly) to array ``["python", "python-aiohttp"]``. | ||||||
|  |  | ||||||
| Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch: | Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch: | ||||||
|  |  | ||||||
| #. | #. | ||||||
|  | |||||||
| @ -102,8 +102,9 @@ class Patch(Handler): | |||||||
|             patch = "".join(list(sys.stdin)) |             patch = "".join(list(sys.stdin)) | ||||||
|         else: |         else: | ||||||
|             patch = patch_path.read_text(encoding="utf8") |             patch = patch_path.read_text(encoding="utf8") | ||||||
|         patch = patch.strip()  # remove spaces around the patch |         # remove spaces around the patch and parse to correct type | ||||||
|         return PkgbuildPatch(variable, patch) |         parsed = PkgbuildPatch.parse(patch.strip()) | ||||||
|  |         return PkgbuildPatch(variable, parsed) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None: |     def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None: | ||||||
|  | |||||||
| @ -56,7 +56,6 @@ __all__ = [ | |||||||
|     "srcinfo_property", |     "srcinfo_property", | ||||||
|     "srcinfo_property_list", |     "srcinfo_property_list", | ||||||
|     "trim_package", |     "trim_package", | ||||||
|     "unquote", |  | ||||||
|     "utcnow", |     "utcnow", | ||||||
|     "walk", |     "walk", | ||||||
| ] | ] | ||||||
| @ -466,38 +465,6 @@ def trim_package(package_name: str) -> str: | |||||||
|     return package_name |     return package_name | ||||||
|  |  | ||||||
|  |  | ||||||
| def unquote(source: str) -> str: |  | ||||||
|     """ |  | ||||||
|     like :func:`shlex.quote()`, but opposite |  | ||||||
|  |  | ||||||
|     Args: |  | ||||||
|         source(str): source string to remove quotes |  | ||||||
|  |  | ||||||
|     Returns: |  | ||||||
|         str: string with quotes removed |  | ||||||
|  |  | ||||||
|     Raises: |  | ||||||
|         ValueError: if no closing quotation |  | ||||||
|     """ |  | ||||||
|     def generator() -> Generator[str, None, None]: |  | ||||||
|         token = None |  | ||||||
|         for char in source: |  | ||||||
|             if token is not None: |  | ||||||
|                 if char == token: |  | ||||||
|                     token = None  # closed quote |  | ||||||
|                 else: |  | ||||||
|                     yield char  # character inside quotes |  | ||||||
|             elif char in ("'", "\""): |  | ||||||
|                 token = char  # first quote found |  | ||||||
|             else: |  | ||||||
|                 yield char  # normal character |  | ||||||
|  |  | ||||||
|         if token is not None: |  | ||||||
|             raise ValueError("No closing quotation") |  | ||||||
|  |  | ||||||
|     return "".join(generator()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def utcnow() -> datetime.datetime: | def utcnow() -> datetime.datetime: | ||||||
|     """ |     """ | ||||||
|     get current time |     get current time | ||||||
|  | |||||||
| @ -21,9 +21,9 @@ import shlex | |||||||
|  |  | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Any, Self | from typing import Any, Generator, Self | ||||||
|  |  | ||||||
| from ahriman.core.util import dataclass_view, unquote | from ahriman.core.util import dataclass_view | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(frozen=True) | @dataclass(frozen=True) | ||||||
| @ -81,14 +81,57 @@ class PkgbuildPatch: | |||||||
|             Self: package properties |             Self: package properties | ||||||
|         """ |         """ | ||||||
|         key, *value_parts = variable.split("=", maxsplit=1) |         key, *value_parts = variable.split("=", maxsplit=1) | ||||||
|  |  | ||||||
|         raw_value = next(iter(value_parts), "")  # extract raw value |         raw_value = next(iter(value_parts), "")  # extract raw value | ||||||
|         if raw_value.startswith("(") and raw_value.endswith(")"): |         return cls(key, cls.parse(raw_value)) | ||||||
|             value: str | list[str] = shlex.split(raw_value[1:-1])  # arrays for poor |  | ||||||
|         else: |  | ||||||
|             value = unquote(raw_value) |  | ||||||
|  |  | ||||||
|         return cls(key, value) |     @staticmethod | ||||||
|  |     def parse(source: str) -> str | list[str]: | ||||||
|  |         """ | ||||||
|  |         parse string value to the PKGBUILD patch value. This method simply takes string, tries to identify it as array | ||||||
|  |         or just string and return the respective value. Functions should be processed correctly, however, not guaranteed | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             source(str): source string to parse | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             str | list[str]: parsed value either string or list of strings | ||||||
|  |         """ | ||||||
|  |         if source.startswith("(") and source.endswith(")"): | ||||||
|  |             return shlex.split(source[1:-1])  # arrays for poor | ||||||
|  |         return PkgbuildPatch.unquote(source) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def unquote(source: str) -> str: | ||||||
|  |         """ | ||||||
|  |         like :func:`shlex.quote()`, but opposite | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             source(str): source string to remove quotes | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             str: string with quotes removed | ||||||
|  |  | ||||||
|  |         Raises: | ||||||
|  |             ValueError: if no closing quotation | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         def generator() -> Generator[str, None, None]: | ||||||
|  |             token = None | ||||||
|  |             for char in source: | ||||||
|  |                 if token is not None: | ||||||
|  |                     if char == token: | ||||||
|  |                         token = None  # closed quote | ||||||
|  |                     else: | ||||||
|  |                         yield char  # character inside quotes | ||||||
|  |                 elif char in ("'", "\""): | ||||||
|  |                     token = char  # first quote found | ||||||
|  |                 else: | ||||||
|  |                     yield char  # normal character | ||||||
|  |  | ||||||
|  |             if token is not None: | ||||||
|  |                 raise ValueError("No closing quotation") | ||||||
|  |  | ||||||
|  |         return "".join(generator()) | ||||||
|  |  | ||||||
|     def serialize(self) -> str: |     def serialize(self) -> str: | ||||||
|         """ |         """ | ||||||
|  | |||||||
| @ -122,11 +122,10 @@ def test_patch_create_from_function(mocker: MockerFixture) -> None: | |||||||
|     """ |     """ | ||||||
|     must create function patch from file |     must create function patch from file | ||||||
|     """ |     """ | ||||||
|     path = Path("local") |  | ||||||
|     patch = PkgbuildPatch("version", "patch") |     patch = PkgbuildPatch("version", "patch") | ||||||
|     read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value) |     read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value) | ||||||
|  |  | ||||||
|     assert Patch.patch_create_from_function(patch.key, path) == patch |     assert Patch.patch_create_from_function(patch.key, Path("local")) == patch | ||||||
|     read_mock.assert_called_once_with(encoding="utf8") |     read_mock.assert_called_once_with(encoding="utf8") | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -148,6 +147,15 @@ def test_patch_create_from_function_strip(mocker: MockerFixture) -> None: | |||||||
|     assert Patch.patch_create_from_function(patch.key, None) == patch |     assert Patch.patch_create_from_function(patch.key, None) == patch | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_patch_create_from_function_array(mocker: MockerFixture) -> None: | ||||||
|  |     """ | ||||||
|  |     must correctly read array variable | ||||||
|  |     """ | ||||||
|  |     patch = PkgbuildPatch("version", ["array", "patch"]) | ||||||
|  |     mocker.patch("pathlib.Path.read_text", return_value=f"({" ".join(patch.value)})") | ||||||
|  |     assert Patch.patch_create_from_function(patch.key, Path("local")) == patch | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_patch_set_list(application: Application, mocker: MockerFixture) -> None: | def test_patch_set_list(application: Application, mocker: MockerFixture) -> None: | ||||||
|     """ |     """ | ||||||
|     must list available patches for the command |     must list available patches for the command | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ from unittest.mock import call as MockCall | |||||||
| from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError | from ahriman.core.exceptions import BuildError, CalledProcessError, OptionError, UnsafeRunError | ||||||
| from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \ | from ahriman.core.util import check_output, check_user, dataclass_view, enum_values, extract_user, filter_json, \ | ||||||
|     full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \ |     full_version, minmax, package_like, parse_version, partition, pretty_datetime, pretty_size, safe_filename, \ | ||||||
|     srcinfo_property, srcinfo_property_list, trim_package, unquote, utcnow, walk |     srcinfo_property, srcinfo_property_list, trim_package, utcnow, walk | ||||||
| from ahriman.models.package import Package | from ahriman.models.package import Package | ||||||
| from ahriman.models.package_source import PackageSource | from ahriman.models.package_source import PackageSource | ||||||
| from ahriman.models.repository_id import RepositoryId | from ahriman.models.repository_id import RepositoryId | ||||||
| @ -445,26 +445,6 @@ def test_trim_package() -> None: | |||||||
|     assert trim_package("package: a description") == "package" |     assert trim_package("package: a description") == "package" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unquote() -> None: |  | ||||||
|     """ |  | ||||||
|     must remove quotation marks |  | ||||||
|     """ |  | ||||||
|     for source in ( |  | ||||||
|         "abc", |  | ||||||
|         "ab'c", |  | ||||||
|         "ab\"c", |  | ||||||
|     ): |  | ||||||
|         assert unquote(shlex.quote(source)) == source |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unquote_error() -> None: |  | ||||||
|     """ |  | ||||||
|     must raise value error on invalid quotation |  | ||||||
|     """ |  | ||||||
|     with pytest.raises(ValueError): |  | ||||||
|         unquote("ab'c") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_utcnow() -> None: | def test_utcnow() -> None: | ||||||
|     """ |     """ | ||||||
|     must generate correct timestamp |     must generate correct timestamp | ||||||
|  | |||||||
| @ -1,3 +1,6 @@ | |||||||
|  | import pytest | ||||||
|  | import shlex | ||||||
|  |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from pytest_mock import MockerFixture | from pytest_mock import MockerFixture | ||||||
| from unittest.mock import MagicMock, call | from unittest.mock import MagicMock, call | ||||||
| @ -48,6 +51,35 @@ def test_from_env() -> None: | |||||||
|     assert PkgbuildPatch.from_env("KEY") == PkgbuildPatch("KEY", "") |     assert PkgbuildPatch.from_env("KEY") == PkgbuildPatch("KEY", "") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_parse() -> None: | ||||||
|  |     """ | ||||||
|  |     must parse string correctly | ||||||
|  |     """ | ||||||
|  |     assert PkgbuildPatch.parse("VALUE") == "VALUE" | ||||||
|  |     assert PkgbuildPatch.parse("(ARRAY VALUE)") == ["ARRAY", "VALUE"] | ||||||
|  |     assert PkgbuildPatch.parse("""("QU'OUTED" ARRAY VALUE)""") == ["QU'OUTED", "ARRAY", "VALUE"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_unquote() -> None: | ||||||
|  |     """ | ||||||
|  |     must remove quotation marks | ||||||
|  |     """ | ||||||
|  |     for source in ( | ||||||
|  |         "abc", | ||||||
|  |         "ab'c", | ||||||
|  |         "ab\"c", | ||||||
|  |     ): | ||||||
|  |         assert PkgbuildPatch.unquote(shlex.quote(source)) == source | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_unquote_error() -> None: | ||||||
|  |     """ | ||||||
|  |     must raise value error on invalid quotation | ||||||
|  |     """ | ||||||
|  |     with pytest.raises(ValueError): | ||||||
|  |         PkgbuildPatch.unquote("ab'c") | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_serialize() -> None: | def test_serialize() -> None: | ||||||
|     """ |     """ | ||||||
|     must correctly serialize string values |     must correctly serialize string values | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user