PEP-673 use Self as return type for classmethods (#94)

* PEP-673 use Self as return type for classmethods

* add dummy test file

* remove python3.10 compat
This commit is contained in:
Evgenii Alekseev 2023-05-04 03:28:08 +03:00 committed by GitHub
parent 9dc6d56a8d
commit 2ff56965d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 384 additions and 339 deletions

View File

@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never'
# refresh the image # refresh the image
pacman --noconfirm -Syu pacman --noconfirm -Syu
# main dependencies # main dependencies
pacman --noconfirm -Sy base-devel devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo sudo pacman --noconfirm -Sy base-devel devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo sudo
# make dependencies # make dependencies
pacman --noconfirm -Sy python-build python-installer python-wheel pacman --noconfirm -Sy python-build python-installer python-wheel
# optional dependencies # optional dependencies

View File

@ -4,7 +4,7 @@
set -ex set -ex
# install dependencies # install dependencies
pacman --noconfirm -Syu base-devel python-pip python-setuptools python-tox pacman --noconfirm -Syu base-devel python-setuptools python-tox
# run test and check targets # run test and check targets
make check tests make check tests

View File

@ -90,8 +90,9 @@ Again, the most checks can be performed by `make check` command, though some add
self.instance_attribute = "" self.instance_attribute = ""
``` ```
* Type annotations are the must, even for local functions. * Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`). * For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`).
* `classmethod` should always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
* Recommended order of function definitions in class: * Recommended order of function definitions in class:
```python ```python
@ -103,7 +104,7 @@ Again, the most checks can be performed by `make check` command, though some add
def property(self) -> Any: ... def property(self) -> Any: ...
@classmethod @classmethod
def class_method(cls: type[Clazz]) -> Clazz: ... def class_method(cls) -> Self: ...
@staticmethod @staticmethod
def static_method() -> Any: ... def static_method() -> Any: ...

View File

@ -28,7 +28,7 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package" COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies ## install package dependencies
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size ## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-setuptools python-srcinfo && \ RUN pacman --noconfirm -Sy devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \
pacman --noconfirm -Sy python-build python-installer python-wheel && \ pacman --noconfirm -Sy python-build python-installer python-wheel && \
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \ pacman --noconfirm -Sy breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \ runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \

View File

@ -7,7 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-setuptools' 'python-srcinfo') depends=('devtools' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
makedepends=('python-build' 'python-installer' 'python-wheel') makedepends=('python-build' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support' optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support' 'darcs: -darcs packages support'

View File

@ -34,7 +34,6 @@ setup(
"inflection", "inflection",
"passlib", "passlib",
"requests", "requests",
"setuptools",
"srcinfo", "srcinfo",
], ],
setup_requires=[ setup_requires=[

View File

@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.models.pacman_synchronization import PacmanSynchronization
class ApplicationProperties(LazyLogging): class ApplicationProperties(LazyLogging):
@ -34,8 +35,8 @@ class ApplicationProperties(LazyLogging):
repository(Repository): repository instance repository(Repository): repository instance
""" """
def __init__(self, architecture: str, configuration: Configuration, *, def __init__(self, architecture: str, configuration: Configuration, *, report: bool, unsafe: bool,
report: bool, unsafe: bool, refresh_pacman_database: int = 0) -> None: refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> None:
""" """
default constructor default constructor
@ -44,8 +45,8 @@ class ApplicationProperties(LazyLogging):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
refresh_pacman_database(int, optional): pacman database syncronization level, ``0`` is disabled refresh_pacman_database(PacmanSynchronization, optional): pacman database synchronization level
(Default value = 0) (Default value = PacmanSynchronization.Disabled)
""" """
self.configuration = configuration self.configuration = configuration
self.architecture = architecture self.architecture = architecture

View File

@ -30,7 +30,7 @@ class Add(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -36,7 +36,7 @@ class Backup(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -30,7 +30,7 @@ class Clean(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -30,7 +30,7 @@ class Daemon(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -32,7 +32,7 @@ class Dump(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -17,8 +17,6 @@
# 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/>.
# #
from __future__ import annotations
import argparse import argparse
import logging import logging
@ -52,7 +50,7 @@ class Handler:
ALLOW_MULTI_ARCHITECTURE_RUN = True ALLOW_MULTI_ARCHITECTURE_RUN = True
@classmethod @classmethod
def architectures_extract(cls: type[Handler], args: argparse.Namespace) -> list[str]: def architectures_extract(cls, args: argparse.Namespace) -> list[str]:
""" """
get known architectures get known architectures
@ -83,7 +81,7 @@ class Handler:
return sorted(architectures) return sorted(architectures)
@classmethod @classmethod
def call(cls: type[Handler], args: argparse.Namespace, architecture: str) -> bool: def call(cls, args: argparse.Namespace, architecture: str) -> bool:
""" """
additional function to wrap all calls for multiprocessing library additional function to wrap all calls for multiprocessing library
@ -108,7 +106,7 @@ class Handler:
return False return False
@classmethod @classmethod
def execute(cls: type[Handler], args: argparse.Namespace) -> int: def execute(cls, args: argparse.Namespace) -> int:
""" """
execute function for all aru execute function for all aru
@ -137,7 +135,7 @@ class Handler:
return 0 if all(result) else 1 return 0 if all(result) else 1
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -31,7 +31,7 @@ class Help(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -32,7 +32,7 @@ class KeyImport(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -38,7 +38,7 @@ class Patch(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -32,7 +32,7 @@ class Rebuild(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -30,7 +30,7 @@ class Remove(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -31,7 +31,7 @@ class RemoveUnknown(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -33,7 +33,7 @@ class Restore(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -40,10 +40,14 @@ class Search(Handler):
""" """
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
SORT_FIELDS = {field.name for field in fields(AURPackage) if field.default_factory is not list} # type: ignore SORT_FIELDS = {
field.name
for field in fields(AURPackage)
if field.default_factory is not list # type: ignore[comparison-overlap]
}
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -35,7 +35,7 @@ class ServiceUpdates(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -45,7 +45,7 @@ class Setup(Handler):
SUDOERS_DIR_PATH = Path("/etc/sudoers.d") SUDOERS_DIR_PATH = Path("/etc/sudoers.d")
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line
@ -153,7 +153,7 @@ class Setup(Handler):
configuration = Configuration(allow_no_value=True) configuration = Configuration(allow_no_value=True)
# preserve case # preserve case
# stupid mypy thinks that it is impossible # stupid mypy thinks that it is impossible
configuration.optionxform = lambda key: key # type: ignore configuration.optionxform = lambda key: key # type: ignore[method-assign]
# load default configuration first # load default configuration first
# we cannot use Include here because it will be copied to new chroot, thus no includes there # we cannot use Include here because it will be copied to new chroot, thus no includes there

View File

@ -37,7 +37,7 @@ class Shell(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False ALLOW_MULTI_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -30,7 +30,7 @@ class Sign(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -37,7 +37,7 @@ class Status(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -33,7 +33,7 @@ class StatusUpdate(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -34,7 +34,7 @@ class Structure(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -31,7 +31,7 @@ class Triggers(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -33,7 +33,7 @@ class UnsafeCommands(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -32,7 +32,7 @@ class Update(Handler):
""" """
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -39,7 +39,7 @@ class Users(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -39,7 +39,7 @@ class Validate(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -18,9 +18,12 @@
# 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 argparse import argparse
import pkg_resources import re
import sys import sys
from collections.abc import Generator
from importlib import metadata
from ahriman import version from ahriman import version
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -30,12 +33,16 @@ from ahriman.core.formatters import VersionPrinter
class Versions(Handler): class Versions(Handler):
""" """
version handler version handler
Attributes:
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name
""" """
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line
@ -49,37 +56,43 @@ class Versions(Handler):
""" """
VersionPrinter(f"Module version {version.__version__}", VersionPrinter(f"Module version {version.__version__}",
{"Python": sys.version}).print(verbose=False, separator=" ") {"Python": sys.version}).print(verbose=False, separator=" ")
packages = Versions.package_dependencies("ahriman", ("pacman", "s3", "web")) packages = Versions.package_dependencies("ahriman")
VersionPrinter("Installed packages", packages).print(verbose=False, separator=" ") VersionPrinter("Installed packages", dict(packages)).print(verbose=False, separator=" ")
@staticmethod @staticmethod
def package_dependencies(root: str, root_extras: tuple[str, ...] = ()) -> dict[str, str]: def package_dependencies(root: str) -> Generator[tuple[str, str], None, None]:
""" """
extract list of ahriman package dependencies installed into system with their versions extract list of ahriman package dependencies installed into system with their versions
Args: Args:
root(str): root package name root(str): root package name
root_extras(tuple[str, ...], optional): extras for the root package (Default value = ())
Returns: Returns:
dict[str, str]: map of installed dependency to its version Generator[tuple[str, str], None, None]: map of installed dependency to its version
""" """
resources: dict[str, pkg_resources.Distribution] = pkg_resources.working_set.by_key # type: ignore def dependencies_by_key(key: str) -> Generator[str, None, None]:
# in importlib it returns requires in the following format
def dependencies_by_key(key: str, extras: tuple[str, ...] = ()) -> list[str]: # ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
return [entry.key for entry in resources[key].requires(extras)] try:
requires = metadata.requires(key)
except metadata.PackageNotFoundError:
return
for entry in requires or []:
yield from Versions.PEP423_PACKAGE_NAME.findall(entry)
keys: list[str] = [] keys: list[str] = []
portion = {key for key in dependencies_by_key(root, root_extras) if key in resources} portion = set(dependencies_by_key(root))
while portion: while portion:
keys.extend(portion) keys.extend(portion)
portion = { portion = {
key key
for key in sum((dependencies_by_key(key) for key in portion), start=[]) for key in sum((list(dependencies_by_key(key)) for key in portion), start=[])
if key not in keys and key in resources if key not in keys
} }
return { for key in keys:
resource.project_name: resource.version try:
for resource in map(lambda key: resources[key], keys) distribution = metadata.distribution(key)
} yield distribution.name, distribution.version
except metadata.PackageNotFoundError:
continue

View File

@ -33,7 +33,7 @@ class Web(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
@classmethod @classmethod
def run(cls: type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *, def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None: report: bool, unsafe: bool) -> None:
""" """
callback for command line callback for command line

View File

@ -17,12 +17,10 @@
# 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/>.
# #
from __future__ import annotations
import argparse import argparse
from types import TracebackType from types import TracebackType
from typing import Literal from typing import Literal, Self
from ahriman import version from ahriman import version
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -111,7 +109,7 @@ class Lock(LazyLogging):
except FileExistsError: except FileExistsError:
raise DuplicateRunError() raise DuplicateRunError()
def __enter__(self) -> Lock: def __enter__(self) -> Self:
""" """
default workflow is the following: default workflow is the following:
@ -120,6 +118,9 @@ class Lock(LazyLogging):
3. Check web status watcher status 3. Check web status watcher status
4. Create lock file 4. Create lock file
5. Report to status page if enabled 5. Report to status page if enabled
Returns:
Self: always instance of self
""" """
self.check_user() self.check_user()
self.check_version() self.check_version()

View File

@ -21,12 +21,13 @@ import shutil
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
from pathlib import Path from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore[import]
from typing import Any from typing import Any
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.util import trim_package from ahriman.core.util import trim_package
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -40,28 +41,28 @@ class Pacman(LazyLogging):
handle: Handle handle: Handle
def __init__(self, architecture: str, configuration: Configuration, *, refresh_database: int) -> None: def __init__(self, architecture: str, configuration: Configuration, *,
refresh_database: PacmanSynchronization) -> None:
""" """
default constructor default constructor
Args: Args:
architecture(str): repository architecture architecture(str): repository architecture
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
refresh_database(int): synchronize local cache to remote. If set to ``0``, no synchronization will be refresh_database(PacmanSynchronization): synchronize local cache to remote
enabled, if set to ``1`` - normal synchronization, if set to ``2`` - force synchronization
""" """
self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle( self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle(
architecture, configuration, refresh_database=refresh_database) architecture, configuration, refresh_database=refresh_database)
def __create_handle(self, architecture: str, configuration: Configuration, *, refresh_database: int) -> Handle: def __create_handle(self, architecture: str, configuration: Configuration, *,
refresh_database: PacmanSynchronization) -> Handle:
""" """
create lazy handle function create lazy handle function
Args: Args:
architecture(str): repository architecture architecture(str): repository architecture
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
refresh_database(int): synchronize local cache to remote. If set to ``0``, no synchronization will be refresh_database(PacmanSynchronization): synchronize local cache to remote
enabled, if set to ``1`` - normal synchronization, if set to ``2`` - force synchronization
Returns: Returns:
Handle: fully initialized pacman handle Handle: fully initialized pacman handle
@ -79,7 +80,7 @@ class Pacman(LazyLogging):
self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache) self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache)
if use_ahriman_cache and refresh_database: if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database > 1) self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
return handle return handle

View File

@ -44,6 +44,33 @@ class AUR(Remote):
DEFAULT_RPC_VERSION = "5" DEFAULT_RPC_VERSION = "5"
DEFAULT_TIMEOUT = 30 DEFAULT_TIMEOUT = 30
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:
"""
generate remote git url from the package base
Args
package_base(str): package base
repository(str): repository name
Returns:
str: git url for the specific base
"""
return f"{AUR.DEFAULT_AUR_URL}/{package_base}.git"
@classmethod
def remote_web_url(cls, package_base: str) -> str:
"""
generate remote web url from the package base
Args
package_base(str): package base
Returns:
str: web url for the specific base
"""
return f"{AUR.DEFAULT_AUR_URL}/packages/{package_base}"
@staticmethod @staticmethod
def parse_response(response: dict[str, Any]) -> list[AURPackage]: def parse_response(response: dict[str, Any]) -> list[AURPackage]:
""" """
@ -64,33 +91,6 @@ class AUR(Remote):
raise PackageInfoError(error_details) raise PackageInfoError(error_details)
return [AURPackage.from_json(package) for package in response["results"]] return [AURPackage.from_json(package) for package in response["results"]]
@classmethod
def remote_git_url(cls: type[Remote], package_base: str, repository: str) -> str:
"""
generate remote git url from the package base
Args
package_base(str): package base
repository(str): repository name
Returns:
str: git url for the specific base
"""
return f"{AUR.DEFAULT_AUR_URL}/{package_base}.git"
@classmethod
def remote_web_url(cls: type[Remote], package_base: str) -> str:
"""
generate remote web url from the package base
Args
package_base(str): package base
Returns:
str: web url for the specific base
"""
return f"{AUR.DEFAULT_AUR_URL}/packages/{package_base}"
def make_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]: def make_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]:
""" """
perform request to AUR RPC perform request to AUR RPC

View File

@ -44,6 +44,35 @@ class Official(Remote):
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
DEFAULT_TIMEOUT = 30 DEFAULT_TIMEOUT = 30
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:
"""
generate remote git url from the package base
Args
package_base(str): package base
repository(str): repository name
Returns:
str: git url for the specific base
"""
if repository.lower() in ("core", "extra", "testing", "kde-unstable"):
return "https://github.com/archlinux/svntogit-packages.git" # hardcoded, ok
return "https://github.com/archlinux/svntogit-community.git"
@classmethod
def remote_web_url(cls, package_base: str) -> str:
"""
generate remote web url from the package base
Args
package_base(str): package base
Returns:
str: web url for the specific base
"""
return f"{Official.DEFAULT_ARCHLINUX_URL}/packages/{package_base}"
@staticmethod @staticmethod
def parse_response(response: dict[str, Any]) -> list[AURPackage]: def parse_response(response: dict[str, Any]) -> list[AURPackage]:
""" """
@ -62,35 +91,6 @@ class Official(Remote):
raise PackageInfoError("API validation error") raise PackageInfoError("API validation error")
return [AURPackage.from_repo(package) for package in response["results"]] return [AURPackage.from_repo(package) for package in response["results"]]
@classmethod
def remote_git_url(cls: type[Remote], package_base: str, repository: str) -> str:
"""
generate remote git url from the package base
Args
package_base(str): package base
repository(str): repository name
Returns:
str: git url for the specific base
"""
if repository.lower() in ("core", "extra", "testing", "kde-unstable"):
return "https://github.com/archlinux/svntogit-packages.git" # hardcoded, ok
return "https://github.com/archlinux/svntogit-community.git"
@classmethod
def remote_web_url(cls: type[Remote], package_base: str) -> str:
"""
generate remote web url from the package base
Args
package_base(str): package base
Returns:
str: web url for the specific base
"""
return f"{Official.DEFAULT_ARCHLINUX_URL}/packages/{package_base}"
def make_request(self, *args: str, by: str) -> list[AURPackage]: def make_request(self, *args: str, by: str) -> list[AURPackage]:
""" """
perform request to official repositories RPC perform request to official repositories RPC

View File

@ -17,8 +17,6 @@
# 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/>.
# #
from __future__ import annotations
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -42,7 +40,7 @@ class Remote(LazyLogging):
""" """
@classmethod @classmethod
def info(cls: type[Remote], package_name: str, *, pacman: Pacman) -> AURPackage: def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage:
""" """
get package info by its name get package info by its name
@ -56,7 +54,7 @@ class Remote(LazyLogging):
return cls().package_info(package_name, pacman=pacman) return cls().package_info(package_name, pacman=pacman)
@classmethod @classmethod
def multisearch(cls: type[Remote], *keywords: str, pacman: Pacman) -> list[AURPackage]: def multisearch(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
""" """
search in remote repository by using API with multiple words. This method is required in order to handle search in remote repository by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition, short words will be dropped https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
@ -80,7 +78,7 @@ class Remote(LazyLogging):
return list(packages.values()) return list(packages.values())
@classmethod @classmethod
def remote_git_url(cls: type[Remote], package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:
""" """
generate remote git url from the package base generate remote git url from the package base
@ -97,7 +95,7 @@ class Remote(LazyLogging):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def remote_web_url(cls: type[Remote], package_base: str) -> str: def remote_web_url(cls, package_base: str) -> str:
""" """
generate remote web url from the package base generate remote web url from the package base
@ -113,7 +111,7 @@ class Remote(LazyLogging):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def search(cls: type[Remote], *keywords: str, pacman: Pacman) -> list[AURPackage]: def search(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
""" """
search package in AUR web search package in AUR web

View File

@ -62,8 +62,8 @@ class Auth(LazyLogging):
""" """
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>""" return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
@classmethod @staticmethod
def load(cls: type[Auth], configuration: Configuration, database: SQLite) -> Auth: def load(configuration: Configuration, database: SQLite) -> Auth:
""" """
load authorization module from settings load authorization module from settings
@ -81,7 +81,7 @@ class Auth(LazyLogging):
if provider == AuthSettings.OAuth: if provider == AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth from ahriman.core.auth.oauth import OAuth
return OAuth(configuration, database) return OAuth(configuration, database)
return cls(configuration) return Auth(configuration)
async def check_credentials(self, username: str | None, password: str | None) -> bool: async def check_credentials(self, username: str | None, password: str | None) -> bool:
""" """

View File

@ -20,7 +20,7 @@
from typing import Any from typing import Any
try: try:
import aiohttp_security # type: ignore import aiohttp_security # type: ignore[import]
_has_aiohttp_security = True _has_aiohttp_security = True
except ImportError: except ImportError:
_has_aiohttp_security = False _has_aiohttp_security = False

View File

@ -128,7 +128,7 @@ class OAuth(Mapping):
client.access_token = access_token client.access_token = access_token
user, _ = await client.user_info() user, _ = await client.user_info()
username: str = user.email # type: ignore username: str = user.email # type: ignore[attr-defined]
return username return username
except Exception: except Exception:
self.logger.exception("got exception while performing request") self.logger.exception("got exception while performing request")

View File

@ -17,15 +17,13 @@
# 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/>.
# #
from __future__ import annotations
import configparser import configparser
import shlex import shlex
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Self
from ahriman.core.exceptions import InitializeError from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -113,7 +111,7 @@ class Configuration(configparser.RawConfigParser):
return RepositoryPaths(self.getpath("repository", "root"), architecture) return RepositoryPaths(self.getpath("repository", "root"), architecture)
@classmethod @classmethod
def from_path(cls: type[Configuration], path: Path, architecture: str) -> Configuration: def from_path(cls, path: Path, architecture: str) -> Self:
""" """
constructor with full object initialization constructor with full object initialization
@ -122,7 +120,7 @@ class Configuration(configparser.RawConfigParser):
architecture(str): repository architecture architecture(str): repository architecture
Returns: Returns:
Configuration: configuration instance Self: configuration instance
""" """
configuration = cls() configuration = cls()
configuration.load(path) configuration.load(path)
@ -186,9 +184,9 @@ class Configuration(configparser.RawConfigParser):
# pylint and mypy are too stupid to find these methods # pylint and mypy are too stupid to find these methods
# pylint: disable=missing-function-docstring,multiple-statements,unused-argument # pylint: disable=missing-function-docstring,multiple-statements,unused-argument
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]
def gettype(self, section: str, architecture: str, *, fallback: str | None = None) -> tuple[str, str]: def gettype(self, section: str, architecture: str, *, fallback: str | None = None) -> tuple[str, str]:
""" """

View File

@ -19,7 +19,7 @@
# #
import ipaddress import ipaddress
from cerberus import TypeDefinition, Validator as RootValidator # type: ignore from cerberus import TypeDefinition, Validator as RootValidator # type: ignore[import]
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
@ -74,7 +74,7 @@ class Validator(RootValidator):
bool: value converted to boolean according to configuration rules bool: value converted to boolean according to configuration rules
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
return converted return converted
def _normalize_coerce_integer(self, value: str) -> int: def _normalize_coerce_integer(self, value: str) -> int:

View File

@ -23,6 +23,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.util import package_like from ahriman.core.util import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"] __all__ = ["migrate_data", "steps"]
@ -61,7 +62,7 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
return return
_, architecture = configuration.check_loaded() _, architecture = configuration.check_loaded()
pacman = Pacman(architecture, configuration, refresh_database=False) pacman = Pacman(architecture, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = [] package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):

View File

@ -17,12 +17,11 @@
# 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/>.
# #
from __future__ import annotations
import json import json
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from typing import Self
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations import Migrations from ahriman.core.database.migrations import Migrations
@ -46,7 +45,7 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations,
""" """
@classmethod @classmethod
def load(cls: type[SQLite], configuration: Configuration) -> SQLite: def load(cls, configuration: Configuration) -> Self:
""" """
construct instance from configuration construct instance from configuration
@ -54,7 +53,7 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations,
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
Returns: Returns:
SQLite: fully initialized instance of the database Self: fully initialized instance of the database
""" """
path = cls.database_path(configuration) path = cls.database_path(configuration)
database = cls(path) database = cls(path)

View File

@ -68,7 +68,7 @@ class RemotePullTrigger(Trigger):
self.targets = self.configuration_sections(configuration) self.targets = self.configuration_sections(configuration)
@classmethod @classmethod
def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: def configuration_sections(cls, configuration: Configuration) -> list[str]:
""" """
extract configuration sections from configuration extract configuration sections from configuration

View File

@ -76,7 +76,7 @@ class RemotePushTrigger(Trigger):
self.targets = self.configuration_sections(configuration) self.targets = self.configuration_sections(configuration)
@classmethod @classmethod
def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: def configuration_sections(cls, configuration: Configuration) -> list[str]:
""" """
extract configuration sections from configuration extract configuration sections from configuration

View File

@ -17,10 +17,10 @@
# 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/>.
# #
from __future__ import annotations
import logging import logging
from typing import Self
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -52,7 +52,7 @@ class HttpLogHandler(logging.Handler):
self.suppress_errors = suppress_errors self.suppress_errors = suppress_errors
@classmethod @classmethod
def load(cls, configuration: Configuration, *, report: bool) -> HttpLogHandler: def load(cls, configuration: Configuration, *, report: bool) -> Self:
""" """
install logger. This function creates handler instance and adds it to the handler list in case if no other install logger. This function creates handler instance and adds it to the handler list in case if no other
http handler found http handler found
@ -60,6 +60,9 @@ class HttpLogHandler(logging.Handler):
Args: Args:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
Returns:
Self: logger instance with loaded settings
""" """
root = logging.getLogger() root = logging.getLogger()
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None: if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:

View File

@ -66,8 +66,8 @@ class Report(LazyLogging):
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
@classmethod @staticmethod
def load(cls: type[Report], architecture: str, configuration: Configuration, target: str) -> Report: def load(architecture: str, configuration: Configuration, target: str) -> Report:
""" """
load client from settings load client from settings
@ -93,7 +93,7 @@ class Report(LazyLogging):
if provider == ReportSettings.Telegram: if provider == ReportSettings.Telegram:
from ahriman.core.report.telegram import Telegram from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section) return Telegram(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return Report(architecture, configuration) # should never happen
def generate(self, packages: list[Package], result: Result) -> None: def generate(self, packages: list[Package], result: Result) -> None:
""" """

View File

@ -205,7 +205,7 @@ class ReportTrigger(Trigger):
self.targets = self.configuration_sections(configuration) self.targets = self.configuration_sections(configuration)
@classmethod @classmethod
def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: def configuration_sections(cls, configuration: Configuration) -> list[str]:
""" """
extract configuration sections from configuration extract configuration sections from configuration

View File

@ -17,10 +17,9 @@
# 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/>.
# #
from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from typing import Self
from ahriman.core import _Context, context from ahriman.core import _Context, context
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
@ -32,6 +31,7 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.util import package_like from ahriman.core.util import package_like
from ahriman.models.context_key import ContextKey from ahriman.models.context_key import ContextKey
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
class Repository(Executor, UpdateHandler): class Repository(Executor, UpdateHandler):
@ -58,8 +58,8 @@ class Repository(Executor, UpdateHandler):
""" """
@classmethod @classmethod
def load(cls: type[Repository], architecture: str, configuration: Configuration, database: SQLite, *, def load(cls, architecture: str, configuration: Configuration, database: SQLite, *, report: bool, unsafe: bool,
report: bool, unsafe: bool, refresh_pacman_database: int = 0) -> Repository: refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> Self:
""" """
load instance from argument list load instance from argument list
@ -69,8 +69,11 @@ class Repository(Executor, UpdateHandler):
database(SQLite): database instance database(SQLite): database instance
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
refresh_pacman_database(int, optional): pacman database syncronization level, ``0`` is disabled refresh_pacman_database(PacmanSynchronization, optional): pacman database synchronization level
(Default value = 0) (Default value = PacmanSynchronization.Disabled)
Returns:
Self: fully loaded repository class instance
""" """
instance = cls(architecture, configuration, database, instance = cls(architecture, configuration, database,
report=report, unsafe=unsafe, refresh_pacman_database=refresh_pacman_database) report=report, unsafe=unsafe, refresh_pacman_database=refresh_pacman_database)

View File

@ -27,6 +27,7 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.core.triggers import TriggerLoader from ahriman.core.triggers import TriggerLoader
from ahriman.core.util import check_user from ahriman.core.util import check_user
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -49,8 +50,8 @@ class RepositoryProperties(LazyLogging):
vcs_allowed_age(int): maximal age of the VCS packages before they will be checked vcs_allowed_age(int): maximal age of the VCS packages before they will be checked
""" """
def __init__(self, architecture: str, configuration: Configuration, database: SQLite, *, def __init__(self, architecture: str, configuration: Configuration, database: SQLite, *, report: bool, unsafe: bool,
report: bool, unsafe: bool, refresh_pacman_database: int) -> None: refresh_pacman_database: PacmanSynchronization) -> None:
""" """
default constructor default constructor
@ -60,7 +61,7 @@ class RepositoryProperties(LazyLogging):
database(SQLite): database instance database(SQLite): database instance
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
refresh_pacman_database(int, optional): pacman database syncronization level, ``0`` is disabled refresh_pacman_database(PacmanSynchronization): pacman database synchronization level
""" """
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration

View File

@ -32,8 +32,8 @@ class Client:
base build status reporter client base build status reporter client
""" """
@classmethod @staticmethod
def load(cls: type[Client], configuration: Configuration, *, report: bool) -> Client: def load(configuration: Configuration, *, report: bool) -> Client:
""" """
load client from settings load client from settings
@ -45,7 +45,7 @@ class Client:
Client: client according to current settings Client: client according to current settings
""" """
if not report: if not report:
return cls() return Client()
address = configuration.get("web", "address", fallback=None) address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None) host = configuration.get("web", "host", fallback=None)
@ -58,7 +58,7 @@ class Client:
if address or (host and port) or socket: if address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
return WebClient(configuration) return WebClient(configuration)
return cls() return Client()
def add(self, package: Package, status: BuildStatusEnum) -> None: def add(self, package: Package, status: BuildStatusEnum) -> None:
""" """

View File

@ -131,7 +131,7 @@ class WebClient(Client, LazyLogging):
requests.Session: generated session object requests.Session: generated session object
""" """
if use_unix_socket: if use_unix_socket:
import requests_unixsocket # type: ignore import requests_unixsocket # type: ignore[import]
session: requests.Session = requests_unixsocket.Session() session: requests.Session = requests_unixsocket.Session()
return session return session

View File

@ -17,8 +17,6 @@
# 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/>.
# #
from __future__ import annotations
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.configuration.schema import ConfigurationSchema from ahriman.core.configuration.schema import ConfigurationSchema
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
@ -70,8 +68,7 @@ class Trigger(LazyLogging):
self.configuration = configuration self.configuration = configuration
@classmethod @classmethod
def configuration_schema(cls: type[Trigger], architecture: str, def configuration_schema(cls, architecture: str, configuration: Configuration | None) -> ConfigurationSchema:
configuration: Configuration | None) -> ConfigurationSchema:
""" """
configuration schema based on supplied service configuration configuration schema based on supplied service configuration
@ -102,7 +99,7 @@ class Trigger(LazyLogging):
return result return result
@classmethod @classmethod
def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: def configuration_sections(cls, configuration: Configuration) -> list[str]:
""" """
extract configuration sections from configuration extract configuration sections from configuration
@ -116,8 +113,8 @@ class Trigger(LazyLogging):
This method can be used in order to extract specific configuration sections which are set by user, e.g. This method can be used in order to extract specific configuration sections which are set by user, e.g.
from sources:: from sources::
>>> @staticmethod >>> @classmethod
>>> def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: >>> def configuration_sections(cls, configuration: Configuration) -> list[str]:
>>> return configuration.getlist("report", "target", fallback=[]) >>> return configuration.getlist("report", "target", fallback=[])
""" """
del configuration del configuration

View File

@ -17,8 +17,6 @@
# 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/>.
# #
from __future__ import annotations
import contextlib import contextlib
import os import os
@ -26,6 +24,7 @@ from collections.abc import Generator
from importlib import import_module, machinery from importlib import import_module, machinery
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Self
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExtensionError from ahriman.core.exceptions import ExtensionError
@ -66,7 +65,7 @@ class TriggerLoader(LazyLogging):
self.triggers: list[Trigger] = [] self.triggers: list[Trigger] = []
@classmethod @classmethod
def load(cls: type[TriggerLoader], architecture: str, configuration: Configuration) -> TriggerLoader: def load(cls, architecture: str, configuration: Configuration) -> Self:
""" """
create instance from configuration create instance from configuration
@ -75,7 +74,7 @@ class TriggerLoader(LazyLogging):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
Returns: Returns:
TriggerLoader: fully loaded trigger instance Self: fully loaded trigger instance
""" """
instance = cls() instance = cls()
instance.triggers = [ instance.triggers = [

View File

@ -17,7 +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 boto3 # type: ignore import boto3 # type: ignore[import]
import hashlib import hashlib
import mimetypes import mimetypes

View File

@ -66,8 +66,8 @@ class Upload(LazyLogging):
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
@classmethod @staticmethod
def load(cls: type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload: def load(architecture: str, configuration: Configuration, target: str) -> Upload:
""" """
load client from settings load client from settings
@ -90,7 +90,7 @@ class Upload(LazyLogging):
if provider == UploadSettings.Github: if provider == UploadSettings.Github:
from ahriman.core.upload.github import Github from ahriman.core.upload.github import Github
return Github(architecture, configuration, section) return Github(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return Upload(architecture, configuration) # should never happen
def run(self, path: Path, built_packages: list[Package]) -> None: def run(self, path: Path, built_packages: list[Package]) -> None:
""" """

View File

@ -136,7 +136,7 @@ class UploadTrigger(Trigger):
self.targets = self.configuration_sections(configuration) self.targets = self.configuration_sections(configuration)
@classmethod @classmethod
def configuration_sections(cls: type[Trigger], configuration: Configuration) -> list[str]: def configuration_sections(cls, configuration: Configuration) -> list[str]:
""" """
extract configuration sections from configuration extract configuration sections from configuration

View File

@ -17,15 +17,13 @@
# 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/>.
# #
from __future__ import annotations
import datetime import datetime
import inflection import inflection
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from pyalpm import Package # type: ignore from pyalpm import Package # type: ignore[import]
from typing import Any from typing import Any, Self
from ahriman.core.util import filter_json, full_version from ahriman.core.util import filter_json, full_version
@ -102,7 +100,7 @@ class AURPackage:
keywords: list[str] = field(default_factory=list) keywords: list[str] = field(default_factory=list)
@classmethod @classmethod
def from_json(cls: type[AURPackage], dump: dict[str, Any]) -> AURPackage: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct package descriptor from RPC properties construct package descriptor from RPC properties
@ -110,7 +108,7 @@ class AURPackage:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
AURPackage: AUR package descriptor Self: AUR package descriptor
""" """
# filter to only known fields # filter to only known fields
known_fields = [pair.name for pair in fields(cls)] known_fields = [pair.name for pair in fields(cls)]
@ -118,7 +116,7 @@ class AURPackage:
return cls(**filter_json(properties, known_fields)) return cls(**filter_json(properties, known_fields))
@classmethod @classmethod
def from_pacman(cls: type[AURPackage], package: Package) -> AURPackage: def from_pacman(cls, package: Package) -> Self:
""" """
construct package descriptor from official repository wrapper construct package descriptor from official repository wrapper
@ -126,7 +124,7 @@ class AURPackage:
package(Package): pyalpm package descriptor package(Package): pyalpm package descriptor
Returns: Returns:
AURPackage: AUR package descriptor Self: AUR package descriptor
""" """
return cls( return cls(
id=0, id=0,
@ -155,7 +153,7 @@ class AURPackage:
) )
@classmethod @classmethod
def from_repo(cls: type[AURPackage], dump: dict[str, Any]) -> AURPackage: def from_repo(cls, dump: dict[str, Any]) -> Self:
""" """
construct package descriptor from official repository RPC properties construct package descriptor from official repository RPC properties
@ -163,7 +161,7 @@ class AURPackage:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
AURPackage: AUR package descriptor Self: AUR package descriptor
""" """
return cls( return cls(
id=0, id=0,

View File

@ -36,23 +36,6 @@ class AuthSettings(str, Enum):
Configuration = "configuration" Configuration = "configuration"
OAuth = "oauth2" OAuth = "oauth2"
@classmethod
def from_option(cls: type[AuthSettings], value: str) -> AuthSettings:
"""
construct value from configuration
Args:
value(str): configuration value
Returns:
AuthSettings: parsed value
"""
if value.lower() in ("configuration", "mapping"):
return cls.Configuration
if value.lower() in ("oauth", "oauth2"):
return cls.OAuth
return cls.Disabled
@property @property
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
""" """
@ -64,3 +47,20 @@ class AuthSettings(str, Enum):
if self == AuthSettings.Disabled: if self == AuthSettings.Disabled:
return False return False
return True return True
@staticmethod
def from_option(value: str) -> AuthSettings:
"""
construct value from configuration
Args:
value(str): configuration value
Returns:
AuthSettings: parsed value
"""
if value.lower() in ("configuration", "mapping"):
return AuthSettings.Configuration
if value.lower() in ("oauth", "oauth2"):
return AuthSettings.OAuth
return AuthSettings.Disabled

View File

@ -17,11 +17,9 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from enum import Enum from enum import Enum
from typing import Any from typing import Any, Self
from ahriman.core.util import filter_json, pretty_datetime, utcnow from ahriman.core.util import filter_json, pretty_datetime, utcnow
@ -65,7 +63,7 @@ class BuildStatus:
object.__setattr__(self, "status", BuildStatusEnum(self.status)) object.__setattr__(self, "status", BuildStatusEnum(self.status))
@classmethod @classmethod
def from_json(cls: type[BuildStatus], dump: dict[str, Any]) -> BuildStatus: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct status properties from json dump construct status properties from json dump
@ -73,7 +71,7 @@ class BuildStatus:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
BuildStatus: status properties Self: status properties
""" """
known_fields = [pair.name for pair in fields(cls)] known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields)) return cls(**filter_json(dump, known_fields))

View File

@ -17,10 +17,8 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from typing import Any from typing import Any, Self
from ahriman.core.util import filter_json from ahriman.core.util import filter_json
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
@ -49,7 +47,7 @@ class Counters:
success: int = 0 success: int = 0
@classmethod @classmethod
def from_json(cls: type[Counters], dump: dict[str, Any]) -> Counters: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct counters from json dump construct counters from json dump
@ -57,14 +55,14 @@ class Counters:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
Counters: status counters Self: status counters
""" """
# filter to only known fields # filter to only known fields
known_fields = [pair.name for pair in fields(cls)] known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields)) return cls(**filter_json(dump, known_fields))
@classmethod @classmethod
def from_packages(cls: type[Counters], packages: list[tuple[Package, BuildStatus]]) -> Counters: def from_packages(cls, packages: list[tuple[Package, BuildStatus]]) -> Self:
""" """
construct counters from packages statuses construct counters from packages statuses
@ -72,7 +70,7 @@ class Counters:
packages(list[tuple[Package, BuildStatus]]): list of package and their status as per watcher property packages(list[tuple[Package, BuildStatus]]): list of package and their status as per watcher property
Returns: Returns:
Counters: status counters Self: status counters
""" """
per_status = {"total": len(packages)} per_status = {"total": len(packages)}
for _, status in packages: for _, status in packages:

View File

@ -17,10 +17,8 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from typing import Any from typing import Any, Self
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
@ -46,7 +44,7 @@ class InternalStatus:
version: str | None = None version: str | None = None
@classmethod @classmethod
def from_json(cls: type[InternalStatus], dump: dict[str, Any]) -> InternalStatus: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct internal status from json dump construct internal status from json dump
@ -54,7 +52,7 @@ class InternalStatus:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
InternalStatus: internal status Self: internal status
""" """
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0) counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
build_status = dump.get("status") or {} build_status = dump.get("status") or {}

View File

@ -25,9 +25,9 @@ import copy
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from pyalpm import vercmp # type: ignore from pyalpm import vercmp # type: ignore[import]
from srcinfo.parse import parse_srcinfo # type: ignore from srcinfo.parse import parse_srcinfo # type: ignore[import]
from typing import Any from typing import Any, Self
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
@ -179,7 +179,7 @@ class Package(LazyLogging):
return sorted(packages) return sorted(packages)
@classmethod @classmethod
def from_archive(cls: type[Package], path: Path, pacman: Pacman, remote: RemoteSource | None) -> Package: def from_archive(cls, path: Path, pacman: Pacman, remote: RemoteSource | None) -> Self:
""" """
construct package properties from package archive construct package properties from package archive
@ -189,14 +189,14 @@ class Package(LazyLogging):
remote(RemoteSource): package remote source if applicable remote(RemoteSource): package remote source if applicable
Returns: Returns:
Package: package properties Self: package properties
""" """
package = pacman.handle.load_pkg(str(path)) package = pacman.handle.load_pkg(str(path))
description = PackageDescription.from_package(package, path) description = PackageDescription.from_package(package, path)
return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description}) return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description})
@classmethod @classmethod
def from_aur(cls: type[Package], name: str, pacman: Pacman) -> Package: def from_aur(cls, name: str, pacman: Pacman) -> Self:
""" """
construct package properties from AUR page construct package properties from AUR page
@ -205,7 +205,7 @@ class Package(LazyLogging):
pacman(Pacman): alpm wrapper instance pacman(Pacman): alpm wrapper instance
Returns: Returns:
Package: package properties Self: package properties
""" """
package = AUR.info(name, pacman=pacman) package = AUR.info(name, pacman=pacman)
remote = RemoteSource.from_source(PackageSource.AUR, package.package_base, package.repository) remote = RemoteSource.from_source(PackageSource.AUR, package.package_base, package.repository)
@ -216,7 +216,7 @@ class Package(LazyLogging):
packages={package.name: PackageDescription.from_aur(package)}) packages={package.name: PackageDescription.from_aur(package)})
@classmethod @classmethod
def from_build(cls: type[Package], path: Path, architecture: str) -> Package: def from_build(cls, path: Path, architecture: str) -> Self:
""" """
construct package properties from sources directory construct package properties from sources directory
@ -225,7 +225,7 @@ class Package(LazyLogging):
architecture(str): load package for specific architecture architecture(str): load package for specific architecture
Returns: Returns:
Package: package properties Self: package properties
Raises: Raises:
InvalidPackageInfo: if there are parsing errors InvalidPackageInfo: if there are parsing errors
@ -254,7 +254,7 @@ class Package(LazyLogging):
return cls(base=srcinfo["pkgbase"], version=version, remote=None, packages=packages) return cls(base=srcinfo["pkgbase"], version=version, remote=None, packages=packages)
@classmethod @classmethod
def from_json(cls: type[Package], dump: dict[str, Any]) -> Package: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct package properties from json dump construct package properties from json dump
@ -262,7 +262,7 @@ class Package(LazyLogging):
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
Package: package properties Self: package properties
""" """
packages_json = dump.get("packages") or {} packages_json = dump.get("packages") or {}
packages = { packages = {
@ -273,7 +273,7 @@ class Package(LazyLogging):
return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages) return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages)
@classmethod @classmethod
def from_official(cls: type[Package], name: str, pacman: Pacman, *, use_syncdb: bool = True) -> Package: def from_official(cls, name: str, pacman: Pacman, *, use_syncdb: bool = True) -> Self:
""" """
construct package properties from official repository page construct package properties from official repository page
@ -283,7 +283,7 @@ class Package(LazyLogging):
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True) use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
Returns: Returns:
Package: package properties Self: package properties
""" """
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman) package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman)
remote = RemoteSource.from_source(PackageSource.Repository, package.package_base, package.repository) remote = RemoteSource.from_source(PackageSource.Repository, package.package_base, package.repository)

View File

@ -17,12 +17,10 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from pathlib import Path from pathlib import Path
from pyalpm import Package # type: ignore from pyalpm import Package # type: ignore[import]
from typing import Any from typing import Any, Self
from ahriman.core.util import filter_json, trim_package from ahriman.core.util import filter_json, trim_package
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -99,7 +97,7 @@ class PackageDescription:
return Path(self.filename) if self.filename is not None else None return Path(self.filename) if self.filename is not None else None
@classmethod @classmethod
def from_aur(cls: type[PackageDescription], package: AURPackage) -> PackageDescription: def from_aur(cls, package: AURPackage) -> Self:
""" """
construct properties from AUR package model construct properties from AUR package model
@ -107,7 +105,7 @@ class PackageDescription:
package(AURPackage): AUR package model package(AURPackage): AUR package model
Returns: Returns:
PackageDescription: package properties based on source AUR package Self: package properties based on source AUR package
""" """
return cls( return cls(
depends=package.depends, depends=package.depends,
@ -120,7 +118,7 @@ class PackageDescription:
) )
@classmethod @classmethod
def from_json(cls: type[PackageDescription], dump: dict[str, Any]) -> PackageDescription: def from_json(cls, dump: dict[str, Any]) -> Self:
""" """
construct package properties from json dump construct package properties from json dump
@ -128,14 +126,14 @@ class PackageDescription:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
PackageDescription: package properties Self: package properties
""" """
# filter to only known fields # filter to only known fields
known_fields = [pair.name for pair in fields(cls)] known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields)) return cls(**filter_json(dump, known_fields))
@classmethod @classmethod
def from_package(cls: type[PackageDescription], package: Package, path: Path) -> PackageDescription: def from_package(cls, package: Package, path: Path) -> Self:
""" """
construct class from alpm package class construct class from alpm package class
@ -144,7 +142,7 @@ class PackageDescription:
path(Path): path to package archive path(Path): path to package archive
Returns: Returns:
PackageDescription: package properties based on tarball Self: package properties based on tarball
""" """
return cls( return cls(
architecture=package.arch, architecture=package.arch,

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2023 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 <http://www.gnu.org/licenses/>.
#
from enum import Enum
class PacmanSynchronization(int, Enum):
"""
pacman database synchronization flag
Attributes:
Disabled(PacmanSynchronization): (class attribute) do not synchronize local database
Enabled(PacmanSynchronization): (class attribute) synchronize local database (same as pacman -Sy)
Force(PacmanSynchronization): (class attribute) force synchronize local database (same as pacman -Syy)
"""
Disabled = 0
Enabled = 1
Force = 2

View File

@ -17,11 +17,9 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import asdict, dataclass, fields from dataclasses import asdict, dataclass, fields
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Self
from ahriman.core.util import filter_json from ahriman.core.util import filter_json
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@ -63,7 +61,7 @@ class RemoteSource:
return Path(self.path) return Path(self.path)
@classmethod @classmethod
def from_json(cls: type[RemoteSource], dump: dict[str, Any]) -> RemoteSource | None: def from_json(cls, dump: dict[str, Any]) -> Self | None:
""" """
construct remote source from the json dump (or database row) construct remote source from the json dump (or database row)
@ -71,7 +69,7 @@ class RemoteSource:
dump(dict[str, Any]): json dump body dump(dict[str, Any]): json dump body
Returns: Returns:
RemoteSource | None: remote source Self | None: remote source
""" """
# filter to only known fields # filter to only known fields
known_fields = [pair.name for pair in fields(cls)] known_fields = [pair.name for pair in fields(cls)]
@ -81,8 +79,7 @@ class RemoteSource:
return None return None
@classmethod @classmethod
def from_source(cls: type[RemoteSource], source: PackageSource, package_base: str, def from_source(cls, source: PackageSource, package_base: str, repository: str) -> Self | None:
repository: str) -> RemoteSource | None:
""" """
generate remote source from the package base generate remote source from the package base
@ -92,11 +89,11 @@ class RemoteSource:
repository(str): repository name repository(str): repository name
Returns: Returns:
RemoteSource | None: generated remote source if any, None otherwise Self | None: generated remote source if any, None otherwise
""" """
if source == PackageSource.AUR: if source == PackageSource.AUR:
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
return RemoteSource( return cls(
git_url=AUR.remote_git_url(package_base, repository), git_url=AUR.remote_git_url(package_base, repository),
web_url=AUR.remote_web_url(package_base), web_url=AUR.remote_web_url(package_base),
path=".", path=".",
@ -105,7 +102,7 @@ class RemoteSource:
) )
if source == PackageSource.Repository: if source == PackageSource.Repository:
from ahriman.core.alpm.remote import Official from ahriman.core.alpm.remote import Official
return RemoteSource( return cls(
git_url=Official.remote_git_url(package_base, repository), git_url=Official.remote_git_url(package_base, repository),
web_url=Official.remote_web_url(package_base), web_url=Official.remote_web_url(package_base),
path="trunk", path="trunk",

View File

@ -40,8 +40,8 @@ class ReportSettings(str, Enum):
Console = "console" Console = "console"
Telegram = "telegram" Telegram = "telegram"
@classmethod @staticmethod
def from_option(cls: type[ReportSettings], value: str) -> ReportSettings: def from_option(value: str) -> ReportSettings:
""" """
construct value from configuration construct value from configuration
@ -52,11 +52,11 @@ class ReportSettings(str, Enum):
ReportSettings: parsed value ReportSettings: parsed value
""" """
if value.lower() in ("html",): if value.lower() in ("html",):
return cls.HTML return ReportSettings.HTML
if value.lower() in ("email",): if value.lower() in ("email",):
return cls.Email return ReportSettings.Email
if value.lower() in ("console",): if value.lower() in ("console",):
return cls.Console return ReportSettings.Console
if value.lower() in ("telegram",): if value.lower() in ("telegram",):
return cls.Telegram return ReportSettings.Telegram
return cls.Disabled return ReportSettings.Disabled

View File

@ -17,8 +17,6 @@
# 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/>.
# #
from __future__ import annotations
import os import os
import shutil import shutil
@ -117,7 +115,7 @@ class RepositoryPaths:
return self.owner(self.root) return self.owner(self.root)
@classmethod @classmethod
def known_architectures(cls: type[RepositoryPaths], root: Path) -> set[str]: def known_architectures(cls, root: Path) -> set[str]:
""" """
get known architectures get known architectures

View File

@ -36,8 +36,8 @@ class SignSettings(str, Enum):
Packages = "packages" Packages = "packages"
Repository = "repository" Repository = "repository"
@classmethod @staticmethod
def from_option(cls: type[SignSettings], value: str) -> SignSettings: def from_option(value: str) -> SignSettings:
""" """
construct value from configuration construct value from configuration
@ -48,7 +48,7 @@ class SignSettings(str, Enum):
SignSettings: parsed value SignSettings: parsed value
""" """
if value.lower() in ("package", "packages", "sign-package"): if value.lower() in ("package", "packages", "sign-package"):
return cls.Packages return SignSettings.Packages
if value.lower() in ("repository", "sign-repository"): if value.lower() in ("repository", "sign-repository"):
return cls.Repository return SignSettings.Repository
return cls.Disabled return SignSettings.Disabled

View File

@ -36,8 +36,8 @@ class SmtpSSLSettings(str, Enum):
SSL = "ssl" SSL = "ssl"
STARTTLS = "starttls" STARTTLS = "starttls"
@classmethod @staticmethod
def from_option(cls: type[SmtpSSLSettings], value: str) -> SmtpSSLSettings: def from_option(value: str) -> SmtpSSLSettings:
""" """
construct value from configuration construct value from configuration
@ -48,7 +48,7 @@ class SmtpSSLSettings(str, Enum):
SmtpSSLSettings: parsed value SmtpSSLSettings: parsed value
""" """
if value.lower() in ("ssl", "ssl/tls"): if value.lower() in ("ssl", "ssl/tls"):
return cls.SSL return SmtpSSLSettings.SSL
if value.lower() in ("starttls",): if value.lower() in ("starttls",):
return cls.STARTTLS return SmtpSSLSettings.STARTTLS
return cls.Disabled return SmtpSSLSettings.Disabled

View File

@ -38,8 +38,8 @@ class UploadSettings(str, Enum):
S3 = "s3" S3 = "s3"
Github = "github" Github = "github"
@classmethod @staticmethod
def from_option(cls: type[UploadSettings], value: str) -> UploadSettings: def from_option(value: str) -> UploadSettings:
""" """
construct value from configuration construct value from configuration
@ -50,9 +50,9 @@ class UploadSettings(str, Enum):
UploadSettings: parsed value UploadSettings: parsed value
""" """
if value.lower() in ("rsync",): if value.lower() in ("rsync",):
return cls.Rsync return UploadSettings.Rsync
if value.lower() in ("s3",): if value.lower() in ("s3",):
return cls.S3 return UploadSettings.S3
if value.lower() in ("github",): if value.lower() in ("github",):
return cls.Github return UploadSettings.Github
return cls.Disabled return UploadSettings.Disabled

View File

@ -17,11 +17,10 @@
# 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/>.
# #
from __future__ import annotations
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from passlib.hash import sha512_crypt from passlib.hash import sha512_crypt
from passlib.pwd import genword as generate_password from passlib.pwd import genword as generate_password
from typing import Self
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -66,8 +65,8 @@ class User:
_HASHER = sha512_crypt _HASHER = sha512_crypt
@classmethod @classmethod
def from_option(cls: type[User], username: str | None, password: str | None, def from_option(cls, username: str | None, password: str | None,
access: UserAccess = UserAccess.Read) -> User | None: access: UserAccess = UserAccess.Read) -> Self | None:
""" """
build user descriptor from configuration options build user descriptor from configuration options
@ -77,7 +76,7 @@ class User:
access(UserAccess, optional): optional user access (Default value = UserAccess.Read) access(UserAccess, optional): optional user access (Default value = UserAccess.Read)
Returns: Returns:
User | None: generated user descriptor if all options are supplied and None otherwise Self | None: generated user descriptor if all options are supplied and None otherwise
""" """
if username is None or password is None: if username is None or password is None:
return None return None
@ -114,7 +113,7 @@ class User:
verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy verified = False # the absence of evidence is not the evidence of absence (c) Gin Rummy
return verified return verified
def hash_password(self, salt: str) -> User: def hash_password(self, salt: str) -> Self:
""" """
generate hashed password from plain text generate hashed password from plain text
@ -122,7 +121,7 @@ class User:
salt(str): salt for hashed password salt(str): salt for hashed password
Returns: Returns:
User: user with hashed password to store in configuration Self: user with hashed password to store in configuration
""" """
if not self.password: if not self.password:
# in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider # in case of empty password we leave it empty. This feature is used by any external (like OAuth) provider

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import Application from aiohttp.web import Application
from typing import Any from typing import Any

View File

@ -17,7 +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 aiohttp_cors # type: ignore import aiohttp_cors # type: ignore[import]
from aiohttp.web import Application from aiohttp.web import Application

View File

@ -17,7 +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 aiohttp_security # type: ignore import aiohttp_security # type: ignore[import]
import socket import socket
import types import types

View File

@ -17,9 +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/>.
# #
from __future__ import annotations from aiohttp_cors import CorsViewMixin # type: ignore[import]
from aiohttp_cors import CorsViewMixin # type: ignore
from aiohttp.web import Request, StreamResponse, View from aiohttp.web import Request, StreamResponse, View
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any, TypeVar from typing import Any, TypeVar
@ -89,7 +87,7 @@ class BaseView(View, CorsViewMixin):
return validator return validator
@classmethod @classmethod
async def get_permission(cls: type[BaseView], request: Request) -> UserAccess: async def get_permission(cls, request: Request) -> UserAccess:
""" """
retrieve user permission from the request retrieve user permission from the request
@ -168,7 +166,7 @@ class BaseView(View, CorsViewMixin):
return await self.data_as_json(list_keys or []) return await self.data_as_json(list_keys or [])
# pylint: disable=not-callable,protected-access # pylint: disable=not-callable,protected-access
async def head(self) -> StreamResponse: # type: ignore async def head(self) -> StreamResponse: # type: ignore[return]
""" """
HEAD method implementation based on the result of GET method HEAD method implementation based on the result of GET method
@ -181,7 +179,7 @@ class BaseView(View, CorsViewMixin):
if get_method is not None: if get_method is not None:
# there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005 # there is a bug in pylint, see https://github.com/pylint-dev/pylint/issues/6005
response = await get_method() response = await get_method()
response._body = b"" # type: ignore response._body = b"" # type: ignore[assignment]
return response return response
self._raise_allowed_methods() self._raise_allowed_methods()

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent from aiohttp.web import HTTPBadRequest, HTTPNoContent

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent from aiohttp.web import HTTPBadRequest, HTTPNoContent

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent from aiohttp.web import HTTPBadRequest, HTTPNoContent

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent from aiohttp.web import HTTPBadRequest, HTTPNoContent

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
from collections.abc import Callable from collections.abc import Callable

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPNoContent from aiohttp.web import HTTPNoContent

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPNoContent, Response, json_response from aiohttp.web import HTTPNoContent, Response, json_response

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized from aiohttp.web import HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized

View File

@ -17,7 +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 aiohttp_apispec # type: ignore import aiohttp_apispec # type: ignore[import]
from aiohttp.web import HTTPFound, HTTPUnauthorized from aiohttp.web import HTTPFound, HTTPUnauthorized

View File

@ -163,7 +163,7 @@ def setup_service(architecture: str, configuration: Configuration, spawner: Spaw
application.logger.info("setup debug panel") application.logger.info("setup debug panel")
debug_enabled = configuration.getboolean("web", "debug", fallback=False) debug_enabled = configuration.getboolean("web", "debug", fallback=False)
if debug_enabled: if debug_enabled:
import aiohttp_debugtoolbar # type: ignore import aiohttp_debugtoolbar # type: ignore[import]
aiohttp_debugtoolbar.setup(application, aiohttp_debugtoolbar.setup(application,
hosts=configuration.getlist("web", "debug_allowed_hosts", fallback=[]), hosts=configuration.getlist("web", "debug_allowed_hosts", fallback=[]),
check_host=configuration.getboolean("web", "debug_check_host", fallback=False)) check_host=configuration.getboolean("web", "debug_check_host", fallback=False))

View File

@ -15,7 +15,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
print_mock = mocker.patch("ahriman.core.formatters.Printer.print") print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
Versions.run(args, "x86_64", configuration, report=False, unsafe=False) Versions.run(args, "x86_64", configuration, report=False, unsafe=False)
application_mock.assert_called_once_with("ahriman", ("pacman", "s3", "web")) application_mock.assert_called_once_with("ahriman")
print_mock.assert_has_calls([MockCall(verbose=False, separator=" "), MockCall(verbose=False, separator=" ")]) print_mock.assert_has_calls([MockCall(verbose=False, separator=" "), MockCall(verbose=False, separator=" ")])
@ -23,7 +23,7 @@ def test_package_dependencies() -> None:
""" """
must extract package dependencies must extract package dependencies
""" """
packages = Versions.package_dependencies("srcinfo") packages = dict(Versions.package_dependencies("srcinfo"))
assert packages assert packages
assert packages.get("parse") is not None assert packages.get("parse") is not None
@ -32,7 +32,7 @@ def test_package_dependencies_missing() -> None:
""" """
must extract package dependencies even if some of them are missing must extract package dependencies even if some of them are missing
""" """
packages = Versions.package_dependencies("ahriman", ("docs", "pacman", "s3", "web")) packages = dict(Versions.package_dependencies("ahriman"))
assert packages assert packages
assert packages.get("pyalpm") is not None assert packages.get("pyalpm") is not None
assert packages.get("Sphinx") is None assert packages.get("Sphinx") is None

View File

@ -68,7 +68,7 @@ def schema_request(handler: Callable[..., Awaitable[Any]], *, location: str = "j
Returns: Returns:
Schema: request schema as set by the decorators Schema: request schema as set by the decorators
""" """
schemas: list[dict[str, Any]] = handler.__schemas__ # type: ignore schemas: list[dict[str, Any]] = handler.__schemas__ # type: ignore[attr-defined]
return next(schema["schema"] for schema in schemas if schema["put_into"] == location) return next(schema["schema"] for schema in schemas if schema["put_into"] == location)
@ -84,7 +84,7 @@ def schema_response(handler: Callable[..., Awaitable[Any]], *, code: int = 200)
Returns: Returns:
Schema: response schema as set by the decorators Schema: response schema as set by the decorators
""" """
schemas: dict[int, Any] = handler.__apispec__["responses"] # type: ignore schemas: dict[int, Any] = handler.__apispec__["responses"] # type: ignore[attr-defined]
schema = schemas[code]["schema"] schema = schemas[code]["schema"]
if callable(schema): if callable(schema):
schema = schema() schema = schema()

View File

@ -1,22 +1,25 @@
import pytest import pytest
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from typing import Any
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.api.swagger import SwaggerView from ahriman.web.views.api.swagger import SwaggerView
def _client(client: TestClient) -> TestClient: def _client(client: TestClient, mocker: MockerFixture) -> TestClient:
""" """
generate test client with docs generate test client with docs. Thanks to deprecation, we can't change application state since it was run
Args: Args:
client(TestClient): test client fixture client(TestClient): test client fixture
mocker(MockerFixture): mocker object
Returns: Returns:
TestClient: test client fixture with additional properties TestClient: test client fixture with additional properties
""" """
client.app["swagger_dict"] = { swagger_dict = {
"paths": { "paths": {
"/api/v1/logout": { "/api/v1/logout": {
"get": { "get": {
@ -62,6 +65,14 @@ def _client(client: TestClient) -> TestClient:
}, },
], ],
} }
source = client.app.__getitem__
def getitem(name: str) -> Any:
if name == "swagger_dict":
return swagger_dict
return source(name)
mocker.patch("aiohttp.web.Application.__getitem__", side_effect=getitem)
return client return client
@ -75,11 +86,11 @@ async def test_get_permission() -> None:
assert await SwaggerView.get_permission(request) == UserAccess.Unauthorized assert await SwaggerView.get_permission(request) == UserAccess.Unauthorized
async def test_get(client: TestClient) -> None: async def test_get(client: TestClient, mocker: MockerFixture) -> None:
""" """
must generate api-docs correctly must generate api-docs correctly
""" """
client = _client(client) client = _client(client, mocker)
response = await client.get("/api-docs/swagger.json") response = await client.get("/api-docs/swagger.json")
assert response.ok assert response.ok