Compare commits

...

5 Commits

20 changed files with 868 additions and 698 deletions

View File

@ -8,6 +8,10 @@ on:
- '*' - '*'
- '!*rc*' - '!*rc*'
permissions:
contents: read
packages: write
jobs: jobs:
docker-image: docker-image:

View File

@ -2,6 +2,9 @@ name: Regress
on: workflow_dispatch on: workflow_dispatch
permissions:
contents: read
jobs: jobs:
run-regress-tests: run-regress-tests:

View File

@ -5,6 +5,9 @@ on:
tags: tags:
- '*' - '*'
permissions:
contents: write
jobs: jobs:
make-release: make-release:

View File

@ -8,6 +8,9 @@ on:
branches: branches:
- master - master
permissions:
contents: read
jobs: jobs:
run-setup-minimal: run-setup-minimal:

View File

@ -10,6 +10,9 @@ on:
schedule: schedule:
- cron: 1 0 * * * - cron: 1 0 * * *
permissions:
contents: read
jobs: jobs:
run-tests: run-tests:

View File

@ -9,13 +9,7 @@ build:
python: python:
install: install:
- method: pip - requirements: docs/requirements.txt
path: .
extra_requirements:
- docs
- s3
- validator
- web
formats: formats:
- pdf - pdf

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,8 @@ import sys
from pathlib import Path from pathlib import Path
from ahriman import __version__
# support package imports
basedir = Path(__file__).resolve().parent.parent / "src" basedir = Path(__file__).resolve().parent.parent / "src"
sys.path.insert(0, str(basedir)) sys.path.insert(0, str(basedir))
@ -29,6 +28,7 @@ copyright = f"2021-{datetime.date.today().year}, ahriman team"
author = "ahriman team" author = "ahriman team"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
from ahriman import __version__
release = __version__ release = __version__
@ -91,7 +91,13 @@ autoclass_content = "both"
autodoc_member_order = "groupwise" autodoc_member_order = "groupwise"
autodoc_mock_imports = ["cryptography", "pyalpm"] autodoc_mock_imports = [
"aioauth_client",
"aiohttp_security",
"aiohttp_session",
"cryptography",
"pyalpm",
]
autodoc_default_options = { autodoc_default_options = {
"no-undoc-members": True, "no-undoc-members": True,

128
docs/requirements.txt Normal file
View File

@ -0,0 +1,128 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --group ../pyproject.toml:docs --extra s3 --extra validator --extra web --output-file ../docs/requirements.txt ../pyproject.toml
aiohappyeyeballs==2.6.1
# via aiohttp
aiohttp==3.11.18
# via
# ahriman (../pyproject.toml)
# aiohttp-cors
# aiohttp-jinja2
aiohttp-cors==0.8.1
# via ahriman (../pyproject.toml)
aiohttp-jinja2==1.6
# via ahriman (../pyproject.toml)
aiosignal==1.3.2
# via aiohttp
alabaster==1.0.0
# via sphinx
argparse-manpage==4.6
# via ahriman (../pyproject.toml:docs)
attrs==25.3.0
# via aiohttp
babel==2.17.0
# via sphinx
bcrypt==4.3.0
# via ahriman (../pyproject.toml)
boto3==1.38.11
# via ahriman (../pyproject.toml)
botocore==1.38.11
# via
# boto3
# s3transfer
cerberus==1.3.7
# via ahriman (../pyproject.toml)
certifi==2025.4.26
# via requests
charset-normalizer==3.4.2
# via requests
docutils==0.21.2
# via
# sphinx
# sphinx-argparse
# sphinx-rtd-theme
frozenlist==1.6.0
# via
# aiohttp
# aiosignal
idna==3.10
# via
# requests
# yarl
imagesize==1.4.1
# via sphinx
inflection==0.5.1
# via ahriman (../pyproject.toml)
jinja2==3.1.6
# via
# aiohttp-jinja2
# sphinx
jmespath==1.0.1
# via
# boto3
# botocore
markupsafe==3.0.2
# via jinja2
multidict==6.4.3
# via
# aiohttp
# yarl
packaging==25.0
# via sphinx
propcache==0.3.1
# via
# aiohttp
# yarl
pydeps==3.0.1
# via ahriman (../pyproject.toml:docs)
pyelftools==0.32
# via ahriman (../pyproject.toml)
pygments==2.19.1
# via sphinx
python-dateutil==2.9.0.post0
# via botocore
requests==2.32.3
# via
# ahriman (../pyproject.toml)
# sphinx
roman-numerals-py==3.1.0
# via sphinx
s3transfer==0.12.0
# via boto3
shtab==1.7.2
# via ahriman (../pyproject.toml:docs)
six==1.17.0
# via python-dateutil
snowballstemmer==3.0.0.1
# via sphinx
sphinx==8.2.3
# via
# ahriman (../pyproject.toml:docs)
# sphinx-argparse
# sphinx-rtd-theme
# sphinxcontrib-jquery
sphinx-argparse==0.5.2
# via ahriman (../pyproject.toml:docs)
sphinx-rtd-theme==3.0.2
# via ahriman (../pyproject.toml:docs)
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jquery==4.1
# via sphinx-rtd-theme
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
stdlib-list==0.11.1
# via pydeps
urllib3==2.4.0
# via
# botocore
# requests
yarl==1.20.0
# via aiohttp

View File

@ -2,7 +2,7 @@
pkgbase='ahriman' pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web') pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.17.1 pkgver=2.18.0
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@ -635,6 +635,7 @@ _set_new_action() {
# ${!x} -> ${hello} -> "world" # ${!x} -> ${hello} -> "world"
_shtab_ahriman() { _shtab_ahriman() {
local completing_word="${COMP_WORDS[COMP_CWORD]}" local completing_word="${COMP_WORDS[COMP_CWORD]}"
local previous_word="${COMP_WORDS[COMP_CWORD-1]}"
local completed_positional_actions local completed_positional_actions
local current_action local current_action
local current_action_args_start_index local current_action_args_start_index
@ -691,6 +692,10 @@ _shtab_ahriman() {
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
# optional argument started: use option strings # optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
elif [[ "${previous_word}" == ">" || "${previous_word}" == ">>" ||
"${previous_word}" =~ ^[12]">" || "${previous_word}" =~ ^[12]">>" ]]; then
# handle redirection operators
COMPREPLY=( $(compgen -f -- "${completing_word}") )
else else
# use choices & compgen # use choices & compgen
local IFS=$'\n' # items may contain spaces, so delimit using newline local IFS=$'\n' # items may contain spaces, so delimit using newline

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2025\-06\-13" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS

View File

@ -25,15 +25,64 @@ dependencies = [
dynamic = ["version"] dynamic = ["version"]
[project.optional-dependencies]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
reports = [
"Jinja2",
]
s3 = [
"boto3",
]
shell = [
"IPython"
]
stats = [
"matplotlib",
]
unixsocket = [
"requests-unixsocket2", # required by unix socket support
]
validator = [
"cerberus",
]
web = [
"aiohttp",
"aiohttp_cors",
"aiohttp_jinja2",
]
web_api-docs = [
"ahriman[web]",
"aiohttp-apispec",
"setuptools", # required by aiohttp-apispec
]
web_auth = [
"ahriman[web]",
"aiohttp_session",
"aiohttp_security",
"cryptography",
]
web_oauth2 = [
"ahriman[web_auth]",
"aioauth-client",
]
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.urls] [project.urls]
Documentation = "https://ahriman.readthedocs.io/" Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman" Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases" Changelog = "https://github.com/arcan1s/ahriman/releases"
[project.scripts] [dependency-groups]
ahriman = "ahriman.application.ahriman:run"
[project.optional-dependencies]
check = [ check = [
"autopep8", "autopep8",
"bandit", "bandit",
@ -47,24 +96,6 @@ docs = [
"shtab", "shtab",
"sphinx-argparse", "sphinx-argparse",
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734 "sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
s3 = [
"boto3",
]
shell = [
"IPython"
]
stats = [
"matplotlib",
] ]
tests = [ tests = [
"pytest", "pytest",
@ -75,22 +106,6 @@ tests = [
"pytest-resource-path", "pytest-resource-path",
"pytest-spec", "pytest-spec",
] ]
validator = [
"cerberus",
]
web = [
"Jinja2",
"aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket2", # required by unix socket support
"setuptools", # required by aiohttp-apispec
]
[tool.flit.sdist] [tool.flit.sdist]
include = [ include = [

View File

@ -17,4 +17,4 @@
# 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/>.
# #
__version__ = "2.17.1" __version__ = "2.18.0"

View File

@ -23,7 +23,7 @@ import sys
from collections.abc import Generator, Mapping, MutableMapping from collections.abc import Generator, Mapping, MutableMapping
from string import Template from string import Template
from typing import ClassVar from typing import Any, ClassVar
from ahriman.core.configuration.shell_template import ShellTemplate from ahriman.core.configuration.shell_template import ShellTemplate
@ -85,7 +85,7 @@ class ShellInterpolator(configparser.Interpolation):
"prefix": sys.prefix, "prefix": sys.prefix,
} }
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str, def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: Any, option: Any, value: str,
defaults: Mapping[str, str]) -> str: defaults: Mapping[str, str]) -> str:
""" """
interpolate option value interpolate option value
@ -100,8 +100,8 @@ class ShellInterpolator(configparser.Interpolation):
Args: Args:
parser(MutableMapping[str, Mapping[str, str]]): option parser parser(MutableMapping[str, Mapping[str, str]]): option parser
section(str): section name section(Any): section name
option(str): option name option(Any): option name
value(str): source (not-converted) value value(str): source (not-converted) value
defaults(Mapping[str, str]): default values defaults(Mapping[str, str]): default values

View File

@ -95,19 +95,6 @@ class DuplicateRunError(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.") self, "Another application instance is run. This error can be suppressed by using --force flag.")
class EncodeError(ValueError):
"""
exception used for bytes encoding errors
"""
def __init__(self, encodings: list[str]) -> None:
"""
Args:
encodings(list[str]): list of encodings tried
"""
ValueError.__init__(self, f"Could not encode bytes by using {encodings}")
class ExitCode(RuntimeError): class ExitCode(RuntimeError):
""" """
special exception which has to be thrown to return non-zero status without error message special exception which has to be thrown to return non-zero status without error message

View File

@ -24,7 +24,6 @@ from pathlib import Path
from typing import Any, ClassVar, IO, Self from typing import Any, ClassVar, IO, Self
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -34,13 +33,13 @@ class Pkgbuild(Mapping[str, Any]):
model and proxy for PKGBUILD properties model and proxy for PKGBUILD properties
Attributes: Attributes:
DEFAULT_ENCODINGS(list[str]): (class attribute) list of encoding to be applied on the file content DEFAULT_ENCODINGS(str): (class attribute) default encoding to be applied on the file content
fields(dict[str, PkgbuildPatch]): PKGBUILD fields fields(dict[str, PkgbuildPatch]): PKGBUILD fields
""" """
fields: dict[str, PkgbuildPatch] fields: dict[str, PkgbuildPatch]
DEFAULT_ENCODINGS: ClassVar[list[str]] = ["utf8", "latin-1"] DEFAULT_ENCODINGS: ClassVar[str] = "utf8"
@property @property
def variables(self) -> dict[str, str]: def variables(self) -> dict[str, str]:
@ -58,13 +57,13 @@ class Pkgbuild(Mapping[str, Any]):
} }
@classmethod @classmethod
def from_file(cls, path: Path, encodings: list[str] | None = None) -> Self: def from_file(cls, path: Path, encoding: str | None = None) -> Self:
""" """
parse PKGBUILD from the file parse PKGBUILD from the file
Args: Args:
path(Path): path to the PKGBUILD file path(Path): path to the PKGBUILD file
encodings(list[str] | None, optional): the encoding of the file (Default value = None) encoding(str | None, optional): the encoding of the file (Default value = None)
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
@ -77,15 +76,10 @@ class Pkgbuild(Mapping[str, Any]):
content = input_file.read() content = input_file.read()
# decode bytes content based on either # decode bytes content based on either
encodings = encodings or cls.DEFAULT_ENCODINGS encoding = encoding or cls.DEFAULT_ENCODINGS
for encoding in encodings: io = StringIO(content.decode(encoding, errors="backslashreplace"))
try:
io = StringIO(content.decode(encoding))
return cls.from_io(io)
except ValueError:
pass
raise EncodeError(encodings) return cls.from_io(io)
@classmethod @classmethod
def from_io(cls, stream: IO[str]) -> Self: def from_io(cls, stream: IO[str]) -> Self:

View File

@ -3,9 +3,7 @@ import pytest
from io import BytesIO, StringIO from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -46,18 +44,6 @@ def test_from_file_latin(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> N
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_from_file_unknown_encoding(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
"""
must raise exception when encoding is unknown
"""
open_mock = mocker.patch("pathlib.Path.open")
io_mock = open_mock.return_value.__enter__.return_value = MagicMock()
io_mock.read.return_value.decode.side_effect = EncodeError(pkgbuild_ahriman.DEFAULT_ENCODINGS)
with pytest.raises(EncodeError):
assert Pkgbuild.from_file(Path("local"))
def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
""" """
must correctly load from io must correctly load from io

View File

@ -1,8 +1,8 @@
import pytest import pytest
import pytest_asyncio
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from aiohttp.web import Application, Resource, UrlMappingMatchInfo from aiohttp.web import Application, Resource, UrlMappingMatchInfo
from asyncio import BaseEventLoop
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from marshmallow import Schema from marshmallow import Schema
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -164,15 +164,13 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
return application return application
@pytest.fixture @pytest_asyncio.fixture
def client(application: Application, event_loop: BaseEventLoop, aiohttp_client: Any, async def client(application: Application, aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
mocker: MockerFixture) -> TestClient:
""" """
web client fixture web client fixture
Args: Args:
application(Application): application fixture application(Application): application fixture
event_loop(BaseEventLoop): context event loop
aiohttp_client(Any): aiohttp client fixture aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object mocker(MockerFixture): mocker object
@ -180,37 +178,35 @@ def client(application: Application, event_loop: BaseEventLoop, aiohttp_client:
TestClient: web client test instance TestClient: web client test instance
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[])
return event_loop.run_until_complete(aiohttp_client(application)) return await aiohttp_client(application)
@pytest.fixture @pytest_asyncio.fixture
def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any, async def client_with_auth(application_with_auth: Application, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions
Args:
application_with_auth(Application): application fixture
event_loop(BaseEventLoop): context event loop
aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object
Returns:
TestClient: web client test instance
"""
mocker.patch("pathlib.Path.iterdir", return_value=[])
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
@pytest.fixture
def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient: mocker: MockerFixture) -> TestClient:
""" """
web client fixture with full authorization functions web client fixture with full authorization functions
Args: Args:
application_with_auth(Application): application fixture application_with_auth(Application): application fixture
event_loop(BaseEventLoop): context event loop aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object
Returns:
TestClient: web client test instance
"""
mocker.patch("pathlib.Path.iterdir", return_value=[])
return await aiohttp_client(application_with_auth)
@pytest_asyncio.fixture
async def client_with_oauth_auth(application_with_auth: Application, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions
Args:
application_with_auth(Application): application fixture
aiohttp_client(Any): aiohttp client fixture aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object mocker(MockerFixture): mocker object
@ -219,4 +215,4 @@ def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseE
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[])
application_with_auth[AuthKey] = MagicMock(spec=OAuth) application_with_auth[AuthKey] = MagicMock(spec=OAuth)
return event_loop.run_until_complete(aiohttp_client(application_with_auth)) return await aiohttp_client(application_with_auth)

46
tox.ini
View File

@ -1,9 +1,9 @@
[tox] [tox]
envlist = check, tests envlist = check, tests
isolated_build = True isolated_build = true
labels = labels =
release = version, docs, publish release = version, docs, publish
dependencies = -e .[journald,pacman,s3,shell,stats,validator,web] dependencies = -e .[journald,pacman,reports,s3,shell,stats,unixsocket,validator,web,web_api-docs,web_auth,web_oauth2]
project_name = ahriman project_name = ahriman
[mypy] [mypy]
@ -24,10 +24,13 @@ commands =
[testenv:check] [testenv:check]
description = Run common checks like linter, mypy, etc description = Run common checks like linter, mypy, etc
dependency_groups =
check
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[check] pip_pre = true
setenv = setenv =
CFLAGS="-Wno-unterminated-string-initialization"
MYPYPATH=src MYPYPATH=src
commands = commands =
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/{[tox]project_name}" "tests/{[tox]project_name}" autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/{[tox]project_name}" "tests/{[tox]project_name}"
@ -38,16 +41,19 @@ commands =
[testenv:docs] [testenv:docs]
description = Generate source files for documentation description = Generate source files for documentation
depends =
version
deps =
{[tox]dependencies}
-e .[docs]
changedir = src
allowlist_externals = allowlist_externals =
bash bash
find find
mv mv
changedir = src
dependency_groups =
docs
depends =
version
deps =
{[tox]dependencies}
uv
pip_pre = true
setenv = setenv =
SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance
commands = commands =
@ -59,22 +65,26 @@ commands =
# remove autogenerated modules rst files # remove autogenerated modules rst files
find ../docs -type f -name "{[tox]project_name}*.rst" -delete find ../docs -type f -name "{[tox]project_name}*.rst" -delete
sphinx-apidoc -o ../docs . sphinx-apidoc -o ../docs .
# 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] [testenv:html]
description = Generate html documentation description = Generate html documentation
dependency_groups =
docs
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[docs] pip_pre = true
recreate = True recreate = true
commands = commands =
sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html
[testenv:publish] [testenv:publish]
description = Create and publish release to GitHub description = Create and publish release to GitHub
depends =
docs
allowlist_externals = allowlist_externals =
git git
depends =
docs
passenv = passenv =
SSH_AUTH_SOCK SSH_AUTH_SOCK
commands = commands =
@ -86,18 +96,22 @@ commands =
[testenv:tests] [testenv:tests]
description = Run tests description = Run tests
dependency_groups =
tests
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[tests] pip_pre = true
setenv =
CFLAGS="-Wno-unterminated-string-initialization"
commands = commands =
pytest {posargs} pytest {posargs}
[testenv:version] [testenv:version]
description = Bump package version description = Bump package version
deps =
packaging
allowlist_externals = allowlist_externals =
sed sed
deps =
packaging
commands = commands =
# check if version is set and validate it # check if version is set and validate it
{envpython} -c 'from packaging.version import Version; Version("{posargs}")' {envpython} -c 'from packaging.version import Version; Version("{posargs}")'