Compare commits

..

2 Commits

25 changed files with 6261 additions and 6175 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.15.2
pkgver=2.15.1
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-09\-26" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2024\-09\-24" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -989,25 +989,7 @@ usage: ahriman web [\-h]
start web server
.SH COMMENTS
Quick setup command (replace repository name, architecture and packager as needed):
>>> ahriman \-a x86_64 \-r aur service\-setup \-\-packager "ahriman bot <ahriman@example.com>"
Add new package from AUR:
>>> ahriman package\-add ahriman \-\-now
Check for updates and build out\-of\-dated packages (add ``\-\-dry\-run`` to build it later):
>>> ahriman repo\-update
Remove package from the repository:
>>> ahriman package\-remove ahriman
Start web service (requires additional configuration):
>>> ahriman web
Argument list can also be read from file by using @ prefix.
.SH AUTHOR
.nf

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "2.15.2"
__version__ = "2.15.1"

View File

@ -141,7 +141,7 @@ class Setup(Handler):
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w", encoding="utf8") as ahriman_configuration:
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)
@staticmethod
@ -191,7 +191,7 @@ class Setup(Handler):
configuration.set_option(repository_id.name, "Server", repository_server)
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf"
with target.open("w", encoding="utf8") as devtools_configuration:
with target.open("w") as devtools_configuration:
configuration.write(devtools_configuration)
@staticmethod

View File

@ -112,7 +112,7 @@ class Lock(LazyLogging):
"""
if self.path is None:
return
self._pid_file = self.path.open("a+", encoding="utf8")
self._pid_file = self.path.open("a+")
def _watch(self) -> bool:
"""

View File

@ -91,8 +91,9 @@ class Pacman(LazyLogging):
database = self.database_init(handle, repository, self.repository_id.architecture)
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
# install repository database too (without copying)
self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
# install repository database too
local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
self.database_copy(handle, local_database, pacman_root, use_ahriman_cache=use_ahriman_cache)
if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
@ -114,7 +115,6 @@ class Pacman(LazyLogging):
if not use_ahriman_cache:
return
# copy root database if no local copy found
pacman_db_path = Path(handle.dbpath)
if not pacman_db_path.is_dir():
@ -123,13 +123,11 @@ class Pacman(LazyLogging):
if dst.is_file():
return # file already exists, do not copy
dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist
src = repository_database(pacman_root)
if not src.is_file():
self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name)
return # database for some reason deos not exist
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
self.logger.info("copy pacman database from operating system root to ahriman's home")
shutil.copy(src, dst)
self.repository_paths.chown(dst)

View File

@ -174,31 +174,18 @@ class PkgbuildParser(shlex.shlex):
Returns:
bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise
"""
# wrapper around reading utf symbols from random position of the stream
def read_last() -> tuple[int, str]:
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
current_position = self._io.tell()
last_char = penultimate_char = None
index = current_position - 1
while index > 0:
for index in range(current_position - 1, -1, -1):
self._io.seek(index)
index, last_char = read_last()
last_char = self._io.read(1)
if last_char.isspace():
index -= 1
continue
if index > 1:
if index >= 0:
self._io.seek(index - 1)
_, penultimate_char = read_last()
penultimate_char = self._io.read(1)
break
@ -229,7 +216,6 @@ class PkgbuildParser(shlex.shlex):
case PkgbuildToken.Comment:
self.instream.readline()
continue
yield token
if token != PkgbuildToken.ArrayEnds:
@ -262,28 +248,24 @@ class PkgbuildParser(shlex.shlex):
counter += 1
case PkgbuildToken.FunctionEnds:
end_position = self._io.tell()
if self.state != self.eof: # type: ignore[attr-defined]
end_position -= 1 # if we are not at the end of the file, position is _after_ the token
counter -= 1
if counter == 0:
break
case PkgbuildToken.Comment:
self.instream.readline()
if not 0 < start_position < end_position:
raise PkgbuildParserError("function body wasn't found")
# read the specified interval from source stream
self._io.seek(start_position - 1) # start from the previous symbol
# we cannot use :func:`read()` here, because it reads characters, not bytes
content = ""
while self._io.tell() != end_position and (next_char := self._io.read(1)):
content += next_char
content = self._io.read(end_position - start_position)
# special case of the end of file
if self.state == self.eof: # type: ignore[attr-defined]
content += self._io.read(1)
# reset position (because the last position was before the next token starts)
self._io.seek(end_position)
return content
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:

View File

@ -141,7 +141,7 @@ def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) ->
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open(encoding="utf8") as cache:
with cache_path.open() as cache:
dump = json.load(cache)
for item in dump.get("packages", []):

View File

@ -23,8 +23,9 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T")
@ -38,16 +39,16 @@ class Operations(LazyLogging):
path(Path): path to the database file
"""
def __init__(self, path: Path, configuration: Configuration) -> None:
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
"""
Args:
path(Path): path to the database file
configuration(Configuration): configuration instance
repository_id(RepositoryId): repository unique identifier
repository_paths(RepositoryPaths): repository paths
"""
self.path = path
self._configuration = configuration
_, self._repository_id = configuration.check_loaded()
self._repository_paths = configuration.repository_paths
self._repository_id = repository_id
self._repository_paths = repository_paths
@property
def logger_name(self) -> str:

View File

@ -66,9 +66,10 @@ class SQLite(
Self: fully initialized instance of the database
"""
path = cls.database_path(configuration)
_, repository_id = configuration.check_loaded()
database = cls(path, configuration)
database.init()
database = cls(path, repository_id, configuration.repository_paths)
database.init(configuration)
return database
@ -85,18 +86,23 @@ class SQLite(
"""
return configuration.getpath("settings", "database")
def init(self) -> None:
def init(self, configuration: Configuration) -> None:
"""
perform database migrations
Args:
configuration(Configuration): configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
if self._configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
self._repository_paths.chown(self.path)
paths = configuration.repository_paths
if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""

View File

@ -144,8 +144,7 @@ class UpdateHandler(PackageInfo, Cleaner):
branch="master",
)
with self.suppress_logging():
Sources.fetch(cache_dir, source)
Sources.fetch(cache_dir, source)
remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base)

View File

@ -116,7 +116,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w", encoding="utf8") as source_file:
with source_path.open("w") as source_file:
for key in sorted(set(self.trusted + self.packagers + self.revoked)):
public_key = self.sign.key_export(key)
source_file.write(public_key)
@ -129,7 +129,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w", encoding="utf8") as source_file:
with source_path.open("w") as source_file:
for key in sorted(set(self.revoked)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)
@ -147,7 +147,7 @@ class KeyringGenerator(PkgbuildGenerator):
"""
if not self.trusted:
raise PkgbuildGeneratorError
with source_path.open("w", encoding="utf8") as source_file:
with source_path.open("w") as source_file:
for key in sorted(set(self.trusted)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)

View File

@ -64,7 +64,7 @@ class Pkgbuild(Mapping[str, Any]):
Returns:
Self: constructed instance of self
"""
with path.open(encoding="utf8") as input_file:
with path.open() as input_file:
return cls.from_io(input_file)
@classmethod

View File

@ -199,7 +199,7 @@ class PkgbuildPatch:
Args:
pkgbuild_path(Path): path to PKGBUILD file
"""
with pkgbuild_path.open("a", encoding="utf8") as pkgbuild:
with pkgbuild_path.open("a") as pkgbuild:
pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end
pkgbuild.write(self.serialize())
pkgbuild.write("\n") # append new line after the values

View File

@ -174,8 +174,9 @@ class BaseView(View, CorsViewMixin):
# using if/else in order to suppress mypy warning which doesn't know that
# :func:`aiohttp.web.View._raise_allowed_methods()` raises exception
if get_method is not None:
# there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005
response = await get_method()
response._body = b""
response._body = b"" # type: ignore[assignment]
return response
self._raise_allowed_methods()

View File

@ -63,7 +63,7 @@ def test_open(lock: Lock, mocker: MockerFixture) -> None:
lock.path = Path("ahriman.pid")
lock._open()
open_mock.assert_called_once_with("a+", encoding="utf8")
open_mock.assert_called_once_with("a+")
def test_open_skip(lock: Lock, mocker: MockerFixture) -> None:

View File

@ -42,17 +42,6 @@ def test_expand_array_exception() -> None:
assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"])
def test_is_escaped_exception(resource_path_root: Path) -> None:
"""
must raise PkgbuildParserError if no valid utf symbols found
"""
utf8 = resource_path_root / "models" / "utf8"
with utf8.open(encoding="utf8") as content:
content.seek(2)
with pytest.raises(PkgbuildParserError):
assert not PkgbuildParser(content)._is_escaped()
def test_parse_array() -> None:
"""
must parse array
@ -204,7 +193,7 @@ def test_parse(resource_path_root: Path) -> None:
must parse complex file
"""
pkgbuild = resource_path_root / "models" / "pkgbuild"
with pkgbuild.open(encoding="utf8") as content:
with pkgbuild.open() as content:
parser = PkgbuildParser(content)
assert list(parser.parse()) == [
PkgbuildPatch("var", "value"),
@ -269,13 +258,5 @@ def test_parse(resource_path_root: Path) -> None:
}"""),
PkgbuildPatch("function()", """{
body '}' argument
}"""),
PkgbuildPatch("function()", """{
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}"""),
PkgbuildPatch("function()", """{
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}"""),
]

View File

@ -13,7 +13,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
init_mock = mocker.patch("ahriman.core.database.SQLite.init")
SQLite.load(configuration)
init_mock.assert_called_once_with()
init_mock.assert_called_once_with(configuration)
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
@ -21,18 +21,18 @@ def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixt
must run migrations on init
"""
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init()
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), database._configuration)
database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), configuration)
def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None:
def test_init_skip_migration(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must skip migrations if option is set
"""
database._configuration.set_option("settings", "apply_migrations", "no")
configuration.set_option("settings", "apply_migrations", "no")
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init()
database.init(configuration)
migrate_schema_mock.assert_not_called()

View File

@ -114,7 +114,7 @@ def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture
keyring_generator.trusted = ["trusted", "key"]
keyring_generator._generate_gpg(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8")
open_mock.assert_called_once_with("w")
export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
file_mock.write.assert_has_calls([
MockCall("key"), MockCall("\n"),
@ -134,7 +134,7 @@ def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.revoked = ["revoked"]
keyring_generator._generate_revoked(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8")
open_mock.assert_called_once_with("w")
fingerprint_mock.assert_called_once_with("revoked")
file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
@ -150,7 +150,7 @@ def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.trusted = ["trusted", "trusted"]
keyring_generator._generate_trusted(Path("local"))
open_mock.assert_called_once_with("w", encoding="utf8")
open_mock.assert_called_once_with("w")
fingerprint_mock.assert_called_once_with("trusted")
file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])

View File

@ -474,7 +474,6 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild",
resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8",
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",

View File

@ -26,7 +26,7 @@ def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman)
assert Pkgbuild.from_file(Path("local"))
open_mock.assert_called_once_with(encoding="utf8")
open_mock.assert_called_once_with()
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -149,5 +149,5 @@ def test_write(mocker: MockerFixture) -> None:
open_mock.return_value.__enter__.return_value = file_mock
PkgbuildPatch("key", "value").write(Path("PKGBUILD"))
open_mock.assert_called_once_with("a", encoding="utf8")
open_mock.assert_called_once_with("a")
file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")])

View File

@ -69,30 +69,18 @@ function() {
{ inner shell }
last
}
function() {
function () {
body "{" argument
}
function() {
function () {
body "}" argument
}
function() {
function () {
body '{' argument
}
function() {
function () {
body '}' argument
}
# special case with quotes in comments
function() {
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}
# some random unicode symbols
function() {
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}
# other statements
rm -rf --no-preserve-root /*

View File

@ -1 +0,0 @@
<EFBFBD><EFBFBD>