App, lib, cmds: Fix mypy.explicit-override fallout

This commit adds @override decorators to approximately 300 methods
across 76 files that inherit from base classes such as AbstractCmd,
FileContext, ExecContext, Distro, SSHClient, and others.

The decorator ensures the type checker can verify that overridden
methods have compatible signatures and prevents accidental shadowing
of inherited methods without intent.

Files modified include command classes, library modules, distro
implementations, and SSH client implementations.

Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL with pi.dev v
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-07-22 15:51:14 +02:00
commit 5fa008be5a
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
78 changed files with 263 additions and 81 deletions

View file

@ -6,7 +6,7 @@ import os
import sys
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from typing import TYPE_CHECKING, Any
from typing import override, TYPE_CHECKING, Any
from .AsyncRunner import AsyncRunner
from .log import DEBUG, ERR, NOTICE, log, log_m, set_log_flags, set_log_level
@ -192,6 +192,7 @@ class App: # export
class NoopCompleter(BaseCompleter):
@override
def __call__(self, *args, **kwargs):
return None

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import abc
import sys
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
from .log import ERR
from .Types import LoadTypes, Types
@ -173,14 +173,18 @@ class Cmd(AbstractCmd): # export
self.__description = description if description else help
self.__aliases = aliases if aliases else []
@override
def _name(self) -> str:
return self.__name
@override
def _help(self) -> str:
return self.__help
@override
def _description(self) -> str:
return self.__description
@override
def _aliases(self) -> Iterable[str]:
return self.__aliases

View file

@ -5,7 +5,7 @@ import errno
import sys
from decimal import ROUND_FLOOR, Decimal
from typing import TYPE_CHECKING, NamedTuple
from typing import override, TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
from typing import Type
@ -250,6 +250,7 @@ class ExecContext(Base):
super().__init__(*args, **kwargs)
@classmethod
@override
def create(cls, *args, **kwargs) -> ExecContext:
ret = super().create(*args, **kwargs)
if not isinstance(ret, cls):
@ -451,6 +452,7 @@ class ExecContext(Base):
cc.check_exit_code(ret)
return ret
@override
async def _get(
self, path: str, wd: str | None, throw: bool, verbose: bool | None, title: str
) -> Result:
@ -485,6 +487,7 @@ class ExecContext(Base):
cc.check_exit_code(ret)
return ret
@override
async def _put(
self,
path: str,
@ -565,22 +568,27 @@ class ExecContext(Base):
log(ERR, msg)
return ret
@override
async def _unlink(self, path: str) -> None:
cmd = ['rm', '-f', path]
await self.run(cmd, cmd_input = InputMode.NonInteractive)
@override
async def _erase(self, path: str) -> None:
cmd = ['rm', '-rf', path]
await self.run(cmd, cmd_input = InputMode.NonInteractive)
@override
async def _rename(self, src: str, dst: str) -> None:
cmd = ['mv', src, dst]
await self.run(cmd, cmd_input = InputMode.NonInteractive)
@override
async def _mkdir(self, path: str, mode: int) -> None:
cmd = ['mkdir', path, '-m', self.__mode_str(mode)]
await self.run(cmd, cmd_input = InputMode.NonInteractive)
@override
async def _mktemp(self, tmpl: str, directory: bool) -> str:
cmd = ['mktemp']
if directory:
@ -593,6 +601,7 @@ class ExecContext(Base):
)
return result.stdout_str.strip()
@override
async def _stat(self, path: str, follow_symlinks: bool) -> StatResult:
async def __stat(opts: list[str]) -> Result:
@ -662,6 +671,7 @@ class ExecContext(Base):
assert stdout is not None # Just there to pacify the linter
return _build_stat_result(stdout.rstrip('\n').split(_US), mode_base = 8)
@override
async def _chown(self, path: str, owner: str | None, group: str | None) -> None:
if owner is None and group is None:
raise ValueError(f'Tried to chown("{path}") without owner and group')
@ -674,6 +684,7 @@ class ExecContext(Base):
assert ownership is not None # Impossible, just there to calm the linter
await self.run(['chown', ownership, path], cmd_input = InputMode.NonInteractive)
@override
async def _chmod(self, path: str, mode: int) -> None:
await self.run(
['chmod', self.__mode_str(mode), path],

View file

@ -4,7 +4,7 @@ import abc
from enum import Enum, auto
from functools import cached_property
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from .log import DEBUG, ERR, log
from .Uri import Uri
@ -50,6 +50,7 @@ class FileContext(abc.ABC):
async def __aexit__(self, exc_type, exc, tb):
await self.close()
@override
def __repr__(self) -> str:
return self.__uri.id

View file

@ -1,4 +1,4 @@
from typing import Any
from typing import override, Any
meta_tags = [
'name',
@ -56,6 +56,7 @@ class Package:
self.url = url
self.maintainer = maintainer
@override
def __repr__(self) -> str:
global meta_tags
return '\n'.join([f'{key:<15}: {getattr(self, key)}' for key in meta_tags])

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import abc
import re
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
if TYPE_CHECKING:
from .Package import Package
@ -25,6 +25,7 @@ class PackageFilterString(PackageFilter):
raise Exception(f'Unsupported filter string "{definition}"')
self.__definition = url_rx_str
@override
def _match(self, package: Package) -> bool:
url = package.url
if url is None:

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import abc
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
from .base import Result
@ -20,6 +20,7 @@ class ProcFilter(abc.ABC):
class ProcFilterIdentity(ProcFilter):
@override
async def _run(self, data: bytes | None) -> Result:
return Result(data, None, 0)

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
from .ProcFilter import ProcFilter
@ -13,6 +13,7 @@ class ProcFilterGpg(ProcFilter):
def __init__(self, ec: ExecContext) -> None:
self.__ec = ec
@override
async def _run(self, data: bytes | None) -> Result:
if data is None:
raise Exception('No data for GPG to decrypt')

View file

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import override
class Result:
def __init__(
@ -113,6 +115,7 @@ class Result:
return ret
@override
def __repr__(self) -> str:
return self.__summarize(verbose = False)

View file

@ -9,7 +9,7 @@ from tarfile import TarFile, TarInfo
from .CopyContext import CopyContext
from .ExecContext import ExecContext
from .log import DEBUG, ERR, log
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
if TYPE_CHECKING:
from .base import StatResult
@ -89,6 +89,7 @@ class TarIo(CopyContext):
class TarIoTarFile(TarIo):
@override
async def _extract(self, blob: bytes, root: str | None = None) -> None:
tf = TarFile(fileobj = io.BytesIO(blob))
for info in tf.getmembers():
@ -111,7 +112,16 @@ class TarIoTarFile(TarIo):
class TarIoTarExec(TarIo):
@override
async def _extract(self, blob: bytes, root: str | None = None) -> None:
cmd = ['tar']
if root is not None:
cmd += ['-C', root]
cmd += ['-x', '-f', '-']
await self.dst.run(cmd, cmd_input = blob)
@property
@override
def dst(self) -> ExecContext:
ret = super().dst
if not isinstance(ret, ExecContext):
@ -120,10 +130,3 @@ class TarIoTarExec(TarIo):
'context, which only has a file context'
)
return ret
async def _extract(self, blob: bytes, root: str | None = None) -> None:
cmd = ['tar']
if root is not None:
cmd += ['-C', root]
cmd += ['-x', '-f', '-']
await self.dst.run(cmd, cmd_input = blob)

View file

@ -5,7 +5,7 @@ import os
import re
import sys
from typing import TYPE_CHECKING, Generic, Iterable, TypeVar
from typing import override, TYPE_CHECKING, Generic, Iterable, TypeVar
from .log import ERR, OFF, log, parse_log_level
@ -17,6 +17,7 @@ T = TypeVar('T')
class Types(abc.ABC, Iterable[type[T]], Generic[T]): # export
@override
def __iter__(self) -> Iterator[type[T]]:
return iter(self._classes())
@ -64,6 +65,7 @@ class LoadTypes(Types[T]): # export
if self.__debug_level != OFF:
log(self.__debug_level, *args, **kwargs)
@override
def _stringify(self):
tf = 'None' if self.__type_filter is None else (
', '.join([str(f) for f in self.__type_filter])
@ -74,6 +76,7 @@ class LoadTypes(Types[T]): # export
'mod_names: ' + ', '.join(self.__mod_names),
]
@override
def _classes(self) -> Iterable[type[T]]:
if self.__classes is None:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import copy
from functools import cached_property
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
if TYPE_CHECKING:
import urllib.parse
@ -39,9 +39,11 @@ class Uri:
self.__username: str | None = None
self.__password: str | None = None
@override
def __repr__(self) -> str:
return self.full
@override
def __str__(self) -> str:
return self.safe_full_with_username

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ...Distro import Distro as Base
@ -26,27 +26,32 @@ class Distro(Base):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@override
async def _ref(self) -> None:
raise NotImplementedError(
'distro refresh is not yet implemented for Arch-like distributions'
)
@override
async def _dup(self, download_only: bool) -> None:
args = ['-Su']
if download_only:
args.append('-w')
await self.pacman(args)
@override
async def _reboot_required(self, verbose: bool) -> bool:
raise NotImplementedError(
'distro reboot-required is not yet implemented for Arch-like distributions'
)
@override
async def _select_by_name(self, names: Iterable[str]) -> Iterable[Package]:
raise NotImplementedError(
'distro select is not yet implemented for Arch-like distributions'
)
@override
async def _install(self, names: Iterable[str], only_update: bool) -> None:
if only_update:
raise NotImplementedError('--only-update is not yet implemented for pacman')
@ -54,11 +59,13 @@ class Distro(Base):
args.extend(names)
await self.pacman(args)
@override
async def _delete(self, names: Iterable[str]) -> None:
raise NotImplementedError(
'distro delete not yet implemented for Arch-like distributions'
)
@override
async def _pkg_files(self, name: str) -> Iterable[str]:
raise NotImplementedError(
'distro pkg ls yet implemented for Arch-like distributions'

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import os
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ...Distro import Distro as Base
from ...log import NOTICE, log
@ -38,9 +38,11 @@ class Distro(Base):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@override
async def _ref(self) -> None:
await self.apt_get(['update'])
@override
async def _dup(self, download_only: bool) -> None:
args: list[str] = []
if download_only:
@ -48,6 +50,7 @@ class Distro(Base):
args.append('upgrade')
await self.apt_get(args)
@override
async def _reboot_required(self, verbose: bool) -> bool:
reboot_required = '/run/reboot_required'
if os.path.exists(reboot_required):
@ -64,9 +67,11 @@ class Distro(Base):
log(NOTICE, f"No. {reboot_required} doesn't exist.")
return False
@override
async def _select_by_name(self, names: Iterable[str]) -> Iterable[Package]:
return await query_packages(names, ec = self.ctx)
@override
async def _install(self, names: Iterable[str], only_update: bool) -> None:
args = ['install']
if only_update:
@ -75,8 +80,10 @@ class Distro(Base):
args.extend(names)
await self.apt_get(args)
@override
async def _delete(self, names: Iterable[str]) -> None:
await self.dpkg(['-P', *names], sudo = True)
@override
async def _pkg_files(self, name: str) -> Iterable[str]:
return await list_files(name, ec = self.ctx)

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ...Distro import Distro as Base
from ...pm.rpm import list_files, query_packages, run_rpm
@ -43,9 +43,11 @@ class Distro(Base):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@override
async def _ref(self) -> None:
await self.zypper(['refresh'])
@override
async def _dup(self, download_only: bool) -> None:
args = ['dup']
if download_only:
@ -53,6 +55,7 @@ class Distro(Base):
args += self.__update_extra_opts
await self.zypper(args)
@override
async def _reboot_required(self, verbose: bool) -> bool:
opts = []
if not verbose:
@ -64,22 +67,27 @@ class Distro(Base):
return True
return False
@override
async def _select_by_name(self, names: Iterable[str]) -> Iterable[Package]:
return await query_packages(names, ec = self.ctx)
@override
async def _install(self, names: Iterable[str], only_update: bool) -> None:
cmd = ['update' if only_update else 'install']
cmd += self.__update_extra_opts
cmd += names
await self.zypper(cmd)
@override
async def _install_local_files(
self, paths: Iterable[str], only_update: bool
) -> None:
await self.rpm(['-U', '--reinstall', *paths])
@override
async def _delete(self, names: Iterable[str]) -> None:
await self.rpm(['-e', *names], sudo = True)
@override
async def _pkg_files(self, name: str) -> Iterable[str]:
return await list_files(name, ec = self.ctx)

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ..FileContext import FileContext as Base
@ -28,6 +28,7 @@ class Curl(Base):
self.__ec = ec if ec else __local()
@override
async def _get(
self, path: str, wd: str | None, throw: bool, verbose: bool | None, title: str
) -> Result:

View file

@ -7,7 +7,7 @@ import pwd
import sys
from functools import cache
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ..base import Result, StatResult
from ..ExecContext import ExecContext as Base
@ -25,6 +25,7 @@ class Local(Base):
def _username(self) -> str:
return pwd.getpwuid(os.getuid()).pw_name
@override
async def _run(
self,
cmd: list[str],
@ -155,9 +156,11 @@ class Local(Base):
if cwd is not None:
os.chdir(cwd)
@override
async def _unlink(self, path: str) -> None:
os.unlink(path)
@override
async def _erase(self, path: str) -> None:
if os.path.isdir(path):
import shutil
@ -165,26 +168,33 @@ class Local(Base):
return
os.unlink(path)
@override
async def _rename(self, src: str, dst: str) -> None:
os.rename(src, dst)
@override
async def _mkdir(self, path: str, mode: int) -> None:
os.mkdir(path, mode)
@override
async def _stat(self, path: str, follow_symlinks: bool) -> StatResult:
return StatResult.from_os(os.stat(path, follow_symlinks = follow_symlinks))
@override
async def _file_exists(self, path: str) -> bool:
return os.path.exists(path)
@override
async def _chown(self, path: str, owner: str | None, group: str | None) -> None:
uid = pwd.getpwnam(owner).pw_uid if owner else -1
gid = grp.getgrnam(group).gr_gid if group else -1
os.chown(path, uid, gid)
@override
async def _chmod(self, path: str, mode: int) -> None:
os.chmod(path, mode)
@override
async def _is_dir(self, path: str, follow_symlinks: bool) -> bool:
if (not follow_symlinks) and os.path.islink(path):
return False

View file

@ -5,7 +5,7 @@ import os
import pwd
from enum import Flag, auto
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, override
from ..ExecContext import ExecContext
from ..log import DEBUG, ERR, INFO, NOTICE, get_caller_pos, log, log_m
@ -42,6 +42,7 @@ class SSHClient(ExecContext):
) -> Result:
pass
@override
async def _run(
self,
cmd: list[str],
@ -102,6 +103,7 @@ class SSHClient(ExecContext):
return self.uri.port
@property
@override
def username(self) -> str | None:
return self.uri.username

View file

@ -1,3 +1,4 @@
from typing import override
import asyncio
import os
import shlex
@ -42,10 +43,12 @@ class AsyncSSH(Base):
self.__connect_timeout = connect_timeout
self.__conn: asyncssh.SSHClientConnection | None = None
@override
async def _open(self) -> None:
await super()._open()
await self._conn
@override
async def _close(self) -> None:
if self.__conn is not None:
try:
@ -355,6 +358,7 @@ class AsyncSSH(Base):
stdout = b''.join(stdout_parts) if stdout_parts else None
return Result(stdout, None, exit_code, cmd = cmd)
@override
async def _run_ssh(
self,
cmd: list[str],

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import os
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
from ...base import InputMode
from ...util import run_cmd
@ -48,6 +48,7 @@ class Exec(Base):
self.__askpass_orig[key] = os.getenv(key)
os.environ[key] = val
@override
async def _run_ssh(
self,
cmd: list[str],

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import override, TYPE_CHECKING
# Tolerate missing paramiko imports. jw-pkg is designed to work with what it
# finds.
@ -52,15 +52,18 @@ class Paramiko(Base):
def __scp(self) -> Any:
return paramiko.SCPClient(self.__client.get_transport())
@override
async def _open(self) -> None:
await super()._open()
self.__client
@override
async def _close(self) -> None:
if self.___client is not None:
self.___client.close()
self.___client = None
@override
async def _run_ssh(
self,
cmd: list[str],