Compare commits

..

4 Commits

17 changed files with 79 additions and 90 deletions

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

@ -47,6 +47,7 @@ class LogsRotationTrigger(Trigger):
}, },
}, },
} }
REQUIRES_REPOSITORY = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

View File

@ -336,6 +336,7 @@ class ReportTrigger(Trigger):
}, },
}, },
} }
REQUIRES_REPOSITORY = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

View File

@ -103,6 +103,7 @@ class KeyringTrigger(Trigger):
}, },
}, },
} }
REQUIRES_REPOSITORY = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

View File

@ -90,6 +90,7 @@ class MirrorlistTrigger(Trigger):
}, },
}, },
} }
REQUIRES_REPOSITORY = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

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

@ -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 atexit
import contextlib import contextlib
import os import os
@ -60,17 +61,8 @@ class TriggerLoader(LazyLogging):
def __init__(self) -> None: def __init__(self) -> None:
"""""" """"""
self._on_stop_requested = False
self.triggers: list[Trigger] = [] self.triggers: list[Trigger] = []
def __del__(self) -> None:
"""
custom destructor object which calls on_stop in case if it was requested
"""
if not self._on_stop_requested:
return
self.on_stop()
@classmethod @classmethod
def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self: def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self:
""" """
@ -85,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
@ -250,10 +243,11 @@ class TriggerLoader(LazyLogging):
run triggers on load run triggers on load
""" """
self.logger.debug("executing triggers on start") self.logger.debug("executing triggers on start")
self._on_stop_requested = True
for trigger in self.triggers: for trigger in self.triggers:
with self.__execute_trigger(trigger): with self.__execute_trigger(trigger):
trigger.on_start() trigger.on_start()
# register on_stop call
atexit.register(self.on_stop)
def on_stop(self) -> None: def on_stop(self) -> None:
""" """

View File

@ -160,6 +160,7 @@ class UploadTrigger(Trigger):
}, },
}, },
} }
REQUIRES_REPOSITORY = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

View File

@ -7,6 +7,13 @@ from ahriman.core.status import Client
from ahriman.models.result import Result from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert LogsRotationTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None: def test_configuration_sections(configuration: Configuration) -> None:
""" """
must correctly parse target list must correctly parse target list

View File

@ -5,6 +5,13 @@ from ahriman.core.report import ReportTrigger
from ahriman.models.result import Result from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert ReportTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None: def test_configuration_sections(configuration: Configuration) -> None:
""" """
must correctly parse target list must correctly parse target list

View File

@ -7,6 +7,13 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.support import KeyringTrigger from ahriman.core.support import KeyringTrigger
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert KeyringTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None: def test_configuration_sections(configuration: Configuration) -> None:
""" """
must correctly parse target list must correctly parse target list

View File

@ -4,6 +4,13 @@ from ahriman.core.configuration import Configuration
from ahriman.core.support import MirrorlistTrigger from ahriman.core.support import MirrorlistTrigger
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert MirrorlistTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None: def test_configuration_sections(configuration: Configuration) -> None:
""" """
must correctly parse target list must correctly parse target list

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

@ -153,38 +153,12 @@ def test_on_start(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:
""" """
upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_start") upload_mock = mocker.patch("ahriman.core.upload.UploadTrigger.on_start")
report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_start") report_mock = mocker.patch("ahriman.core.report.ReportTrigger.on_start")
atexit_mock = mocker.patch("atexit.register")
trigger_loader.on_start() trigger_loader.on_start()
assert trigger_loader._on_stop_requested
report_mock.assert_called_once_with() report_mock.assert_called_once_with()
upload_mock.assert_called_once_with() upload_mock.assert_called_once_with()
atexit_mock.assert_called_once_with(trigger_loader.on_stop)
def test_on_stop_with_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must call on_stop on exit if on_start was called
"""
mocker.patch("ahriman.core.upload.UploadTrigger.on_start")
mocker.patch("ahriman.core.report.ReportTrigger.on_start")
on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop")
_, repository_id = configuration.check_loaded()
trigger_loader = TriggerLoader.load(repository_id, configuration)
trigger_loader.on_start()
del trigger_loader
on_stop_mock.assert_called_once_with()
def test_on_stop_without_on_start(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must call not on_stop on exit if on_start wasn't called
"""
on_stop_mock = mocker.patch("ahriman.core.triggers.trigger_loader.TriggerLoader.on_stop")
_, repository_id = configuration.check_loaded()
trigger_loader = TriggerLoader.load(repository_id, configuration)
del trigger_loader
on_stop_mock.assert_not_called()
def test_on_stop(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None: def test_on_stop(trigger_loader: TriggerLoader, mocker: MockerFixture) -> None:

View File

@ -5,6 +5,13 @@ from ahriman.core.upload import UploadTrigger
from ahriman.models.result import Result from ahriman.models.result import Result
def test_requires_repository() -> None:
"""
must require repository identifier to be set to start
"""
assert UploadTrigger.REQUIRES_REPOSITORY
def test_configuration_sections(configuration: Configuration) -> None: def test_configuration_sections(configuration: Configuration) -> None:
""" """
must correctly parse target list must correctly parse target list

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)