diff --git a/.pylint.toml b/.pylint.toml index 85b0815a..95f4628c 100644 --- a/.pylint.toml +++ b/.pylint.toml @@ -1,10 +1,10 @@ [tool.pylint.main] -init-hook = "sys.path.append('pylint_plugins')" +init-hook = "sys.path.append('tools')" load-plugins = [ "pylint.extensions.docparams", "pylint.extensions.bad_builtin", - "definition_order", - "import_order", + "pylint_plugins.definition_order", + "pylint_plugins.import_order", ] [tool.pylint.classes] diff --git a/.pytest.ini b/.pytest.ini new file mode 100644 index 00000000..85345048 --- /dev/null +++ b/.pytest.ini @@ -0,0 +1,5 @@ +[pytest] +addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec +asyncio_default_fixture_loop_scope = function +asyncio_mode = auto +spec_test_format = {result} {docstring_summary} diff --git a/package/share/man/man1/ahriman.1 b/package/share/man/man1/ahriman.1 index 65c25953..5d818711 100644 --- a/package/share/man/man1/ahriman.1 +++ b/package/share/man/man1/ahriman.1 @@ -1,6 +1,6 @@ -.TH AHRIMAN "1" "2025\-06\-16" "ahriman" "Generated Python Manual" +.TH AHRIMAN "1" "2025\-06\-23" "ahriman 2.18.3" "ArcH linux ReposItory MANager" .SH NAME -ahriman +ahriman \- ArcH linux ReposItory MANager .SH SYNOPSIS .B ahriman [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [-V] [--wait-timeout WAIT_TIMEOUT] {add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web} ... diff --git a/pyproject.toml b/pyproject.toml index 6f359b2c..ae839788 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,23 +58,23 @@ web = [ "aiohttp_cors", "aiohttp_jinja2", ] -web_api-docs = [ - "ahriman[web]", - "aiohttp-apispec", - "setuptools", # required by aiohttp-apispec -] -web_auth = [ +web-auth = [ "ahriman[web]", "aiohttp_session", "aiohttp_security", "cryptography", ] -web_metrics = [ +web-docs = [ + "ahriman[web]", + "aiohttp-apispec", + "setuptools", # required by aiohttp-apispec +] +web-metrics = [ "ahriman[web]", "aiohttp-openmetrics", ] -web_oauth2 = [ - "ahriman[web_auth]", +web-oauth2 = [ + "ahriman[web-auth]", "aioauth-client", ] diff --git a/subpackages.py b/subpackages.py index c6910f2f..1e7659fb 100644 --- a/subpackages.py +++ b/subpackages.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2024 ahriman team. +# Copyright (c) 2021-2025 ahriman team. # # This file is part of ahriman # (see https://github.com/arcan1s/ahriman). diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 00000000..7413eea9 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2021-2025 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 . +# diff --git a/pylint_plugins/definition_order.py b/tools/pylint_plugins/definition_order/__init__.py similarity index 99% rename from pylint_plugins/definition_order.py rename to tools/pylint_plugins/definition_order/__init__.py index e86685e5..6237fe9d 100644 --- a/pylint_plugins/definition_order.py +++ b/tools/pylint_plugins/definition_order/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2023 ahriman team. +# Copyright (c) 2021-2025 ahriman team. # # This file is part of ahriman # (see https://github.com/arcan1s/ahriman). diff --git a/pylint_plugins/import_order.py b/tools/pylint_plugins/import_order/__init__.py similarity index 99% rename from pylint_plugins/import_order.py rename to tools/pylint_plugins/import_order/__init__.py index 923cff79..53a981b6 100644 --- a/pylint_plugins/import_order.py +++ b/tools/pylint_plugins/import_order/__init__.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2021-2023 ahriman team. +# Copyright (c) 2021-2025 ahriman team. # # This file is part of ahriman # (see https://github.com/arcan1s/ahriman). diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 886807a4..00000000 --- a/tox.ini +++ /dev/null @@ -1,123 +0,0 @@ -[tox] -envlist = check, tests -isolated_build = true -labels = - release = version, docs, publish -dependencies = -e .[journald,pacman,reports,s3,shell,stats,unixsocket,validator,web,web_api-docs,web_auth,web_oauth2,web_metrics] -project_name = ahriman - -[flags] -autopep8 = --max-line-length 120 -aa --in-place -bandit = --configfile .bandit.yml -manpage = --author "ahriman team" --author-email "" --description "ArcH linux ReposItory MANager" --manual-title "ArcH linux ReposItory MANager" --project-name ahriman --url https://github.com/arcan1s/ahriman -mypy = --implicit-reexport --strict --allow-untyped-decorators --allow-subclassing-any -pydeps = --no-config --cluster -pylint = --rcfile .pylint.toml -shtab = --prefix ahriman --prog ahriman ahriman.application.ahriman._parser - -[pytest] -addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec -asyncio_default_fixture_loop_scope = function -asyncio_mode = auto -spec_test_format = {result} {docstring_summary} - -[testenv:archive] -description = Create source files tarball -deps = - build -commands = - python -m build --sdist - -[testenv:check] -description = Run common checks like linter, mypy, etc -dependency_groups = - check -deps = - {[tox]dependencies} -pip_pre = true -setenv = - CFLAGS="-Wno-unterminated-string-initialization" - MYPYPATH=src -commands = - autopep8 {[flags]autopep8} --exit-code --jobs 0 --recursive "src/{[tox]project_name}" "tests/{[tox]project_name}" - pylint {[flags]pylint} "src/{[tox]project_name}" - bandit {[flags]bandit} --recursive "src/{[tox]project_name}" - bandit {[flags]bandit} --skip B101,B105,B106 --recursive "tests/{[tox]project_name}" - mypy {[flags]mypy} --install-types --non-interactive --package "{[tox]project_name}" - -[testenv:docs] -description = Generate source files for documentation -allowlist_externals = - bash - find -dependency_groups = - docs -depends = - version -deps = - {[tox]dependencies} - uv -pip_pre = true -setenv = - PYTHONPATH=src - SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance -commands = - bash -c 'shtab {[flags]shtab} --shell bash > package/share/bash-completion/completions/_ahriman' - bash -c 'shtab {[flags]shtab} --shell zsh > package/share/zsh/site-functions/_ahriman' - argparse-manpage {[flags]manpage} --module ahriman.application.ahriman --function _parser --output ../package/share/man/man1/ahriman.1 - pydeps {[flags]pydeps} --no-output --show-dot --dot-output {tox_root}{/}docs/_static/architecture.dot src/ahriman - # remove autogenerated modules rst files - find docs -type f -name "{[tox]project_name}*.rst" -delete - sphinx-apidoc --output-dir docs src - # compile list of dependencies for rtd.io - uv pip compile --group pyproject.toml:docs --extra s3 --extra validator --extra web --output-file docs/requirements.txt --quiet pyproject.toml - -[testenv:html] -description = Generate html documentation -dependency_groups = - docs -deps = - {[tox]dependencies} -pip_pre = true -recreate = true -commands = - sphinx-build --builder html --write-all --jobs auto --fail-on-warning docs {envtmpdir}{/}html - -[testenv:publish] -description = Create and publish release to GitHub -allowlist_externals = - git -depends = - docs -passenv = - SSH_AUTH_SOCK -commands = - git add package/archlinux/PKGBUILD src/ahriman/__init__.py docs/_static/architecture.dot package/share/man/man1/ahriman.1 package/share/bash-completion/completions/_ahriman package/share/zsh/site-functions/_ahriman - git commit -m "Release {posargs}" - git tag "{posargs}" - git push - git push --tags - -[testenv:tests] -description = Run tests -dependency_groups = - tests -deps = - {[tox]dependencies} -pip_pre = true -setenv = - CFLAGS="-Wno-unterminated-string-initialization" -commands = - pytest {posargs} - -[testenv:version] -description = Bump package version -allowlist_externals = - sed -deps = - packaging -commands = - # check if version is set and validate it - {envpython} -c 'from packaging.version import Version; Version("{posargs}")' - sed -i 's/^__version__ = .*/__version__ = "{posargs}"/' src/ahriman/__init__.py - sed -i "s/pkgver=.*/pkgver={posargs}/" package/archlinux/PKGBUILD diff --git a/tox.toml b/tox.toml new file mode 100644 index 00000000..594d9762 --- /dev/null +++ b/tox.toml @@ -0,0 +1,310 @@ +env_list = [ + "check", + "tests", +] +isolated_build = true +labels.release = [ + "version", + "docs", + "publish", +] + +[flags] +autopep8 = [ + "--max-line-length", "120", + "-aa", +] +bandit = [ + "--configfile", ".bandit.yml", +] +manpage = [ + "--author", "{[project]name} team", + "--author-email", "", + "--description", "ArcH linux ReposItory MANager", + "--manual-title", "ArcH linux ReposItory MANager", + "--project-name", "{[project]name}", + "--version", "{env:VERSION}", + "--url", "https://github.com/arcan1s/ahriman", +] +mypy = [ + "--implicit-reexport", + "--strict", + "--allow-untyped-decorators", + "--allow-subclassing-any", +] +pydeps = [ + "--no-config", + "--cluster", +] +pylint = [ + "--rcfile", ".pylint.toml", +] +shtab = [ + "--prefix", "{[project]name}", + "--prog", "{[project]name}", + "ahriman.application.ahriman._parser", +] + +[project] +extras = [ + "journald", + "pacman", + "reports", + "s3", + "shell", + "stats", + "unixsocket", + "validator", + "web", + "web-auth", + "web-docs", + "web-oauth2", + "web-metrics", +] +name = "ahriman" + +[env.archive] +description = "Create source files tarball" +deps = [ + "build", +] +commands = [ + [ + "{envpython}", + "-m", "build", + "--sdist", + ], +] + +[env.check] +description = "Run common checks like linter, mypy, etc" +dependency_groups = [ + "check", +] +extras = [ + { replace = "ref", of = ["project", "extras"], extend = true }, +] +pip_pre = true +set_env.CFLAGS = "-Wno-unterminated-string-initialization" +set_env.MYPYPATH = "src" +commands = [ + [ + "autopep8", + { replace = "ref", of = ["flags", "autopep8"], extend = true }, + "--exit-code", + "--in-place", + "--jobs", "0", + "--recursive", + "src/{[project]name}", + "tests/{[project]name}", + ], + [ + "pylint", + { replace = "ref", of = ["flags", "pylint"], extend = true }, + "src/{[project]name}", + ], + [ + "bandit", + { replace = "ref", of = ["flags", "bandit"], extend = true }, + "--recursive", + "src/{[project]name}", + ], + [ + "bandit", + { replace = "ref", of = ["flags", "bandit"], extend = true }, + "--skip", "B101,B105,B106", + "--recursive", + "src/{[project]name}", + ], + [ + "mypy", + { replace = "ref", of = ["flags", "mypy"], extend = true }, + "--install-types", + "--non-interactive", + "--package", "{[project]name}", + ], +] + +[env.docs] +description = "Generate source files for documentation" +dependency_groups = [ + "docs", +] +depends = [ + "version", +] +deps = [ + "uv", +] +dynamic_version = "{[project]name}.__version__" +extras = [ + { 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 +set_env.PYTHONPATH = "src" +set_env.SPHINX_APIDOC_OPTIONS = "members,no-undoc-members,show-inheritance" +commands = [ + [ + "shtab", + { replace = "ref", of = ["flags", "shtab"], extend = true }, + "--shell", + "bash", + ">", + "package/share/bash-completion/completions/_ahriman", + ], + [ + "shtab", + { replace = "ref", of = ["flags", "shtab"], extend = true }, + "--shell", + "zsh", + ">", + "package/share/zsh/site-functions/_ahriman", + ], + [ + "argparse-manpage", + { replace = "ref", of = ["flags", "manpage"], extend = true }, + "--module", "ahriman.application.ahriman", + "--function", "_parser", + "--output", "package/share/man/man1/ahriman.1", + ], + [ + "pydeps", + { replace = "ref", of = ["flags", "pydeps"], extend = true }, + "--dot-output", "{tox_root}/docs/_static/architecture.dot", + "--no-output", + "--show-dot", + "src/ahriman", + ], + [ + "sphinx-apidoc", + "--force", + "--no-toc", + "--output-dir", "docs", + "src", + ], + # compile list of dependencies for rtd.io + [ + "uv", + "pip", + "compile", + "--group", "pyproject.toml:docs", + "--extra", "s3", + "--extra", "validator", + "--extra", "web", + "--output-file", "docs/requirements.txt", + "--quiet", + "pyproject.toml", + ], +] + +[env.html] +description = "Generate html documentation" +dependency_groups = [ + "docs", +] +extras = [ + { replace = "ref", of = ["project", "extras"], extend = true }, +] +pip_pre = true +recreate = true +commands = [ + [ + "sphinx-build", + "--builder", "html", + "--fail-on-warning", + "--jobs", "auto", + "--write-all", + "docs", + "{envtmpdir}/html", + ], +] + +[env.publish] +description = "Create and publish release to GitHub" +allowlist_externals = [ + "git", +] +depends = [ + "docs", +] +pass_env = [ + "SSH_AUTH_SOCK", +] +commands = [ + [ + "git", + "add", + "package/archlinux/PKGBUILD", + "src/ahriman/__init__.py", + "docs/_static/architecture.dot", + "package/share/man/man1/ahriman.1", + "package/share/bash-completion/completions/_ahriman", + "package/share/zsh/site-functions/_ahriman", + ], + [ + "git", + "commit", + "--message", "Release {posargs}", + ], + [ + "git", + "tag", + "{posargs}", + ], + [ + "git", + "push", + ], + [ + "git", + "push", + "--tags", + ], +] + +[env.tests] +description = "Run tests" +dependency_groups = [ + "tests", +] +extras = [ + { replace = "ref", of = ["project", "extras"], extend = true }, +] +pip_pre = true +set_env.CFLAGS = "-Wno-unterminated-string-initialization" +commands = [ + [ + "pytest", + { replace = "posargs", extend = true }, + ], +] + +[env.version] +description = "Bump package version" +allowlist_externals = [ + "sed", +] +deps = [ + "packaging", +] +commands = [ + # check if version is set and validate it + [ + "{envpython}", + "-c", "from packaging.version import Version; Version('{posargs}')", + ], + [ + "sed", + "--in-place", + "s/^__version__ = .*/__version__ = \"{posargs}\"/", + "src/ahriman/__init__.py", + ], + [ + "sed", + "--in-place", + "s/pkgver=.*/pkgver={posargs}/", + "package/archlinux/PKGBUILD", + ], +] diff --git a/toxfile.py b/toxfile.py new file mode 100644 index 00000000..02e9d69c --- /dev/null +++ b/toxfile.py @@ -0,0 +1,128 @@ +# +# Copyright (c) 2021-2025 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 . +# +import importlib +import shlex +import sys + +from tox.config.sets import EnvConfigSet +from tox.config.types import Command +from tox.plugin import impl +from tox.session.state import State +from tox.tox_env.api import ToxEnv + + +def _extract_version(env_conf: EnvConfigSet, python_path: str | None = None) -> dict[str, str]: + """ + extract version dynamically and set VERSION environment variable + + Args: + env_conf(EnvConfigSet): the core configuration object + python_path(str | None): python path variable if available + + Returns: + dict[str, str]: environment variables which must be inserted + """ + import_path = env_conf["dynamic_version"] + if not import_path: + return {} + + if python_path is not None: + sys.path.append(python_path) + + module_name, variable_name = import_path.rsplit(".", maxsplit=1) + module = importlib.import_module(module_name) + version = getattr(module, variable_name) + + # reset import paths + sys.path.pop() + + 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 +def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: + """ + add a command line argument. This is the first hook to be called, + right after the logging setup and config source discovery. + + Args: + env_conf(EnvConfigSet): the core configuration object + state(State): the global tox state object + """ + del state + + env_conf.add_config( + keys=["dynamic_version"], + of_type=str, + default="", + 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 +def tox_before_run_commands(tox_env: ToxEnv) -> None: + """ + called before the commands set is executed + + Args: + tox_env(ToxEnv): the tox environment being executed + """ + env_conf = tox_env.conf + set_env = env_conf["set_env"] + + python_path = set_env.load("PYTHONPATH") if "PYTHONPATH" in set_env else None + set_env.update(_extract_version(env_conf, python_path)) + + _wrap_commands(env_conf)