Compare commits

...

4 Commits

Author SHA1 Message Date
7f813cf0c3 Release 2.18.1 2025-06-16 15:33:24 +03:00
d4eb55ef95 bug: correctly close sqlite3 connection
After the last updates, tests produce warnings that the connection to
database is leaked, which appears to be correct. This commit changes
behaviour to closing connection explicitly via contextlib
2025-06-16 15:24:57 +03:00
09350e88ab style: fix few typos 2025-06-14 23:34:53 +03:00
2feaa14f46 Release 2.18.0 2025-06-13 16:37:58 +03:00
18 changed files with 630 additions and 578 deletions

File diff suppressed because it is too large Load Diff

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.1
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\-16" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS

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.1"

View File

@ -35,7 +35,7 @@ class Remote(SyncHttpClient):
>>> package = AUR.info("ahriman") >>> package = AUR.info("ahriman")
>>> search_result = Official.multisearch("pacman", "manager", pacman=pacman) >>> search_result = Official.multisearch("pacman", "manager", pacman=pacman)
Differnece between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to Difference between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection
between search results. between search results.
""" """

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 contextlib
import sqlite3 import sqlite3
from collections.abc import Callable from collections.abc import Callable
@ -87,10 +88,12 @@ class Operations(LazyLogging):
Returns: Returns:
T: result of the ``query`` call T: result of the ``query`` call
""" """
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection: with contextlib.closing(sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES)) as connection:
connection.set_trace_callback(self.logger.debug) connection.set_trace_callback(self.logger.debug)
connection.row_factory = self.factory connection.row_factory = self.factory
result = query(connection) result = query(connection)
if commit: if commit:
connection.commit() connection.commit()
return result return result

View File

@ -40,7 +40,7 @@ class JinjaTemplate:
* homepage - link to homepage, string, optional * homepage - link to homepage, string, optional
* last_update - report generation time, pretty printed datetime, required * last_update - report generation time, pretty printed datetime, required
* link_path - prefix fo packages to download, string, required * link_path - prefix of packages to download, string, required
* has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required * has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required
* has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required * has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required
* packages - sorted list of packages properties, required * packages - sorted list of packages properties, required
@ -64,7 +64,7 @@ class JinjaTemplate:
Attributes: Attributes:
default_pgp_key(str | None): default PGP key default_pgp_key(str | None): default PGP key
homepage(str | None): homepage link if any (for footer) homepage(str | None): homepage link if any (for footer)
link_path(str): prefix fo packages to download link_path(str): prefix of packages to download
name(str): repository name name(str): repository name
rss_url(str | None): link to the RSS feed rss_url(str | None): link to the RSS feed
sign_targets(set[SignSettings]): targets to sign enabled in configuration sign_targets(set[SignSettings]): targets to sign enabled in configuration

View File

@ -71,7 +71,7 @@ class EventLogger:
>>> with self.in_event(package_base, EventType.PackageUpdated): >>> with self.in_event(package_base, EventType.PackageUpdated):
>>> do_something() >>> do_something()
Additional parameter ``failure`` can be set in order to emit an event on exception occured. If none set Additional parameter ``failure`` can be set in order to emit an event on exception occurred. If none set
(default), then no event will be recorded on exception (default), then no event will be recorded on exception
""" """
with MetricsTimer() as timer: with MetricsTimer() as timer:

View File

@ -69,7 +69,7 @@ class Package(LazyLogging):
:attr:`ahriman.models.package_source.PackageSource.Archive`, :attr:`ahriman.models.package_source.PackageSource.Archive`,
:attr:`ahriman.models.package_source.PackageSource.AUR`, :attr:`ahriman.models.package_source.PackageSource.AUR`,
:attr:`ahriman.models.package_source.PackageSource.Local` and :attr:`ahriman.models.package_source.PackageSource.Local` and
:attr:`ahriman.models.package_source.PackageSource.Repository` repsectively: :attr:`ahriman.models.package_source.PackageSource.Repository` respectively:
>>> ahriman_package = Package.from_aur("ahriman") >>> ahriman_package = Package.from_aur("ahriman")
>>> pacman_package = Package.from_official("pacman", pacman) >>> pacman_package = Package.from_official("pacman", pacman)

View File

@ -39,7 +39,7 @@ class RemoteSchema(Schema):
"example": ".", "example": ".",
}) })
source = fields.Enum(PackageSource, by_value=True, required=True, metadata={ source = fields.Enum(PackageSource, by_value=True, required=True, metadata={
"description": "Pacakge source", "description": "Package source",
}) })
web_url = fields.String(metadata={ web_url = fields.String(metadata={
"description": "Package repository page", "description": "Package repository page",

View File

@ -106,7 +106,7 @@ class PackageView(StatusViewGuard, BaseView):
@apidocs( @apidocs(
tags=["Packages"], tags=["Packages"],
summary="Update package", summary="Update package",
description="Update package status and set its descriptior optionally", description="Update package status and set its descriptor optionally",
permission=POST_PERMISSION, permission=POST_PERMISSION,
error_400_enabled=True, error_400_enabled=True,
error_404_description="Repository is unknown", error_404_description="Repository is unknown",

View File

@ -53,7 +53,7 @@ def test_remote_git_url(remote: Remote) -> None:
must raise NotImplemented for missing remote git url must raise NotImplemented for missing remote git url
""" """
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
remote.remote_git_url("package", "repositorys") remote.remote_git_url("package", "repositories")
def test_remote_web_url(remote: Remote) -> None: def test_remote_web_url(remote: Remote) -> None:

View File

@ -10,7 +10,7 @@ from ahriman.core.exceptions import PacmanError
def test_copy(mocker: MockerFixture) -> None: def test_copy(mocker: MockerFixture) -> None:
""" """
must copy loca database file must copy local database file
""" """
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
PacmanDatabase.copy(Path("remote"), Path("local")) PacmanDatabase.copy(Path("remote"), Path("local"))

View File

@ -1,3 +1,4 @@
import pytest
import sqlite3 import sqlite3
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -24,15 +25,29 @@ def test_factory(database: SQLite) -> None:
def test_with_connection(database: SQLite, mocker: MockerFixture) -> None: def test_with_connection(database: SQLite, mocker: MockerFixture) -> None:
""" """
must run query inside connection must run query inside connection and close it at the end
""" """
connection_mock = MagicMock() connection_mock = MagicMock()
connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock) connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1")) database.with_connection(lambda conn: conn.execute("select 1"))
connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES) connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES)
connection_mock.__enter__().set_trace_callback.assert_called_once_with(database.logger.debug) connection_mock.set_trace_callback.assert_called_once_with(database.logger.debug)
connection_mock.__enter__().commit.assert_not_called() connection_mock.commit.assert_not_called()
connection_mock.close.assert_called_once_with()
def test_with_connection_close(database: SQLite, mocker: MockerFixture) -> None:
"""
must close connection on errors
"""
connection_mock = MagicMock()
connection_mock.commit.side_effect = Exception
mocker.patch("sqlite3.connect", return_value=connection_mock)
with pytest.raises(Exception):
database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.close.assert_called_once_with()
def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None: def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None:
@ -44,4 +59,4 @@ def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) ->
mocker.patch("sqlite3.connect", return_value=connection_mock) mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"), commit=True) database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.__enter__().commit.assert_called_once_with() connection_mock.commit.assert_called_once_with()

View File

@ -285,7 +285,7 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix
def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must skip unknown status update in case if pacakge is already known must skip unknown status update in case if package is already known
""" """
mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)]) mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)])
update_mock = mocker.patch("ahriman.core.status.Client.package_update") update_mock = mocker.patch("ahriman.core.status.Client.package_update")

View File

@ -73,7 +73,7 @@ def test_configuration_sections(configuration: Configuration) -> None:
def test_on_result(trigger: Trigger) -> None: def test_on_result(trigger: Trigger) -> None:
""" """
must pass execution nto run method must pass execution to run method
""" """
trigger.on_result(Result(), []) trigger.on_result(Result(), [])

View File

@ -3,7 +3,7 @@ from ahriman.models.log_record_id import LogRecordId
def test_init() -> None: def test_init() -> None:
""" """
must correctly assign proces identifier if not set must correctly assign process identifier if not set
""" """
assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID
assert LogRecordId("1", "2", "3").process_id == "3" assert LogRecordId("1", "2", "3").process_id == "3"