Compare commits

..

15 Commits

12 changed files with 116 additions and 66 deletions

View File

@ -0,0 +1,29 @@
ahriman.core.archive package
============================
Submodules
----------
ahriman.core.archive.archive\_tree module
-----------------------------------------
.. automodule:: ahriman.core.archive.archive_tree
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.archive.archive\_trigger module
--------------------------------------------
.. automodule:: ahriman.core.archive.archive_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.archive
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -8,6 +8,7 @@ Subpackages
:maxdepth: 4 :maxdepth: 4
ahriman.core.alpm ahriman.core.alpm
ahriman.core.archive
ahriman.core.auth ahriman.core.auth
ahriman.core.build_tools ahriman.core.build_tools
ahriman.core.configuration ahriman.core.configuration

View File

@ -17,7 +17,6 @@
# 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/>.
# #
from ahriman.core import context
from ahriman.core.archive.archive_tree import ArchiveTree from ahriman.core.archive.archive_tree import ArchiveTree
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
@ -45,9 +44,7 @@ class ArchiveTrigger(Trigger):
Trigger.__init__(self, repository_id, configuration) Trigger.__init__(self, repository_id, configuration)
self.paths = configuration.repository_paths self.paths = configuration.repository_paths
self.tree = ArchiveTree(self.paths, GPG(configuration).repository_sign_args)
ctx = context.get()
self.tree = ArchiveTree(self.paths, ctx.get(GPG).repository_sign_args)
def on_result(self, result: Result, packages: list[Package]) -> None: def on_result(self, result: Result, packages: list[Package]) -> None:
""" """

View File

@ -57,7 +57,7 @@ class ConfigurationMultiDict(dict[str, Any]):
OptionError: if the key already exists in the dictionary, but not a single value list or a string OptionError: if the key already exists in the dictionary, but not a single value list or a string
""" """
match self.get(key): match self.get(key):
case [current_value] | str(current_value): case [current_value] | (str() as current_value):
value = f"{current_value} {value}" value = f"{current_value} {value}"
case None: case None:
pass pass

View File

@ -36,6 +36,7 @@ class Trigger(LazyLogging):
CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template
CONFIGURATION_SCHEMA_FALLBACK(str | None): (class attribute) optional fallback option for defining CONFIGURATION_SCHEMA_FALLBACK(str | None): (class attribute) optional fallback option for defining
configuration schema type used configuration schema type used
REQUIRES_REPOSITORY(bool): (class attribute) either trigger requires loaded repository or not
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
@ -59,6 +60,7 @@ class Trigger(LazyLogging):
CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {} CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
REQUIRES_REPOSITORY: ClassVar[bool] = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """
@ -79,6 +81,16 @@ class Trigger(LazyLogging):
""" """
return self.repository_id.architecture return self.repository_id.architecture
@property
def is_allowed_to_run(self) -> bool:
"""
whether trigger allowed to run or not
Returns:
bool: ``True`` in case if trigger allowed to run and ``False`` otherwise
"""
return not (self.REQUIRES_REPOSITORY and self.repository_id.is_empty)
@classmethod @classmethod
def configuration_schema(cls, configuration: Configuration | None) -> ConfigurationSchema: def configuration_schema(cls, configuration: Configuration | None) -> ConfigurationSchema:
""" """

View File

@ -77,8 +77,9 @@ class TriggerLoader(LazyLogging):
""" """
instance = cls() instance = cls()
instance.triggers = [ instance.triggers = [
instance.load_trigger(trigger, repository_id, configuration) trigger
for trigger in instance.selected_triggers(configuration) for trigger_name in instance.selected_triggers(configuration)
if (trigger := instance.load_trigger(trigger_name, repository_id, configuration)).is_allowed_to_run
] ]
return instance return instance

View File

@ -57,7 +57,7 @@ def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
assert check_output_mock.call_args[0][0] == "repo-add" assert check_output_mock.call_args[0][0] == "repo-add"
def test_repo_remove(repo: Repo, package_ahriman: Package,mocker: MockerFixture) -> None: def test_repo_remove(repo: Repo, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must call repo-remove on package removal must call repo-remove on package removal
""" """

View File

@ -1,11 +1,8 @@
import pytest import pytest
from pytest_mock import MockerFixture
from ahriman.core.archive import ArchiveTrigger from ahriman.core.archive import ArchiveTrigger
from ahriman.core.archive.archive_tree import ArchiveTree from ahriman.core.archive.archive_tree import ArchiveTree
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
@pytest.fixture @pytest.fixture
@ -23,18 +20,15 @@ def archive_tree(configuration: Configuration) -> ArchiveTree:
@pytest.fixture @pytest.fixture
def archive_trigger(configuration: Configuration, gpg: GPG, mocker: MockerFixture) -> ArchiveTrigger: def archive_trigger(configuration: Configuration) -> ArchiveTrigger:
""" """
archive trigger fixture archive trigger fixture
Args: Args:
configuration(Configuration): configuration fixture configuration(Configuration): configuration fixture
gpg(GPG): GPG fixture
mocker(MockerFixture): mocker object
Returns: Returns:
ArchiveTrigger: archive trigger test instance ArchiveTrigger: archive trigger test instance
""" """
mocker.patch("ahriman.core._Context.get", return_value=GPG)
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
return ArchiveTrigger(repository_id, configuration) return ArchiveTrigger(repository_id, configuration)

View File

@ -1,4 +1,3 @@
from dataclasses import replace
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -23,6 +22,7 @@ def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, pa
must create symlinks must create symlinks
""" """
_original_exists = Path.exists _original_exists = Path.exists
def exists_mock(path: Path) -> bool: def exists_mock(path: Path) -> bool:
if path.name in (package.filename for package in package_python_schedule.packages.values()): if path.name in (package.filename for package in package_python_schedule.packages.values()):
return True return True
@ -63,6 +63,53 @@ def test_symlinks_create_empty_filename(archive_tree: ArchiveTree, package_ahrim
symlinks_mock.assert_not_called() symlinks_mock.assert_not_called()
def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must fix broken symlinks
"""
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name == "symlink":
return True
return _original_exists(path)
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
walk_mock = mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for() / filename
for filename in ("symlink", "broken_symlink", "file")
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
archive_tree.symlinks_fix()
walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
remove_mock.assert_called_once_with(None, archive_tree.repository_for() / "broken_symlink")
def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must skip symlinks check if repository name or architecture doesn't match
"""
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name == "symlink":
return True
return _original_exists(path)
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for().with_name("i686") / filename
for filename in ("symlink", "broken_symlink", "file")
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
archive_tree.symlinks_fix()
remove_mock.assert_not_called()
def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None: def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
""" """
must create repository root if not exists must create repository root if not exists

View File

@ -3,6 +3,7 @@ from unittest.mock import MagicMock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report import ReportTrigger from ahriman.core.report import ReportTrigger
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result from ahriman.models.result import Result
@ -13,6 +14,19 @@ def test_architecture(trigger: Trigger) -> None:
assert trigger.architecture == trigger.repository_id.architecture assert trigger.architecture == trigger.repository_id.architecture
def test_is_allowed_to_run(trigger: Trigger) -> None:
"""
must return flag correctly
"""
assert trigger.is_allowed_to_run
trigger.repository_id = RepositoryId("", "")
assert not trigger.is_allowed_to_run
trigger.REQUIRES_REPOSITORY = False
assert trigger.is_allowed_to_run
def test_configuration_schema(configuration: Configuration) -> None: def test_configuration_schema(configuration: Configuration) -> None:
""" """
must return used configuration schema must return used configuration schema

View File

@ -140,8 +140,6 @@ dynamic_version = "{[project]name}.__version__"
extras = [ extras = [
{ replace = "ref", of = ["project", "extras"], extend = true }, { replace = "ref", of = ["project", "extras"], extend = true },
] ]
# TODO: steamline shlex usage after https://github.com/iterative/shtab/pull/192 merge
handle_redirect = true
pip_pre = true pip_pre = true
set_env.PYTHONPATH = "src" set_env.PYTHONPATH = "src"
set_env.SPHINX_APIDOC_OPTIONS = "members,no-undoc-members,show-inheritance" set_env.SPHINX_APIDOC_OPTIONS = "members,no-undoc-members,show-inheritance"
@ -149,18 +147,14 @@ commands = [
[ [
"shtab", "shtab",
{ replace = "ref", of = ["flags", "shtab"], extend = true }, { replace = "ref", of = ["flags", "shtab"], extend = true },
"--shell", "--shell", "bash",
"bash", "--output", "package/share/bash-completion/completions/_ahriman",
">",
"package/share/bash-completion/completions/_ahriman",
], ],
[ [
"shtab", "shtab",
{ replace = "ref", of = ["flags", "shtab"], extend = true }, { replace = "ref", of = ["flags", "shtab"], extend = true },
"--shell", "--shell", "zsh",
"zsh", "--output", "package/share/zsh/site-functions/_ahriman",
">",
"package/share/zsh/site-functions/_ahriman",
], ],
[ [
"argparse-manpage", "argparse-manpage",

View File

@ -18,11 +18,9 @@
# 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 importlib import importlib
import shlex
import sys import sys
from tox.config.sets import EnvConfigSet from tox.config.sets import EnvConfigSet
from tox.config.types import Command
from tox.plugin import impl from tox.plugin import impl
from tox.session.state import State from tox.session.state import State
from tox.tox_env.api import ToxEnv from tox.tox_env.api import ToxEnv
@ -56,35 +54,6 @@ def _extract_version(env_conf: EnvConfigSet, python_path: str | None = None) ->
return {"VERSION": version} return {"VERSION": version}
def _wrap_commands(env_conf: EnvConfigSet, shell: str = "bash") -> None:
"""
wrap commands into shell if there is redirect
Args:
env_conf(EnvConfigSet): the core configuration object
shell(str, optional): shell command to use (Default value = "bash")
"""
if not env_conf["handle_redirect"]:
return
# append shell just in case
env_conf["allowlist_externals"].append(shell)
for command in env_conf["commands"]:
if len(command.args) < 3: # command itself, redirect and output
continue
redirect, output = command.args[-2:]
if redirect not in (">", "2>", "&>"):
continue
command.args = [
shell,
"-c",
f"{Command(command.args[:-2]).shell} {redirect} {shlex.quote(output)}",
]
@impl @impl
def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
""" """
@ -103,12 +72,6 @@ def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
default="", default="",
desc="import path for the version variable", desc="import path for the version variable",
) )
env_conf.add_config(
keys=["handle_redirect"],
of_type=bool,
default=False,
desc="wrap commands to handle redirects if any",
)
@impl @impl
@ -124,5 +87,3 @@ def tox_before_run_commands(tox_env: ToxEnv) -> None:
python_path = set_env.load("PYTHONPATH") if "PYTHONPATH" in set_env else None python_path = set_env.load("PYTHONPATH") if "PYTHONPATH" in set_env else None
set_env.update(_extract_version(env_conf, python_path)) set_env.update(_extract_version(env_conf, python_path))
_wrap_commands(env_conf)