jw-pkg/src/python/jw/pkg/lib/TarIo.py
Jan Lindemann 1e613a39c6 App, cmds, lib: Fix Any returns from typed functions
Add type annotations and casts to functions that were returning Any
where a specific type was declared, satisfying the new warn_return_any
mypy rule.

Fixes:
- log.py: get_caller_pos return type via cast
- AsyncRunner.py: cast T for fut.result()
- util.py: cast for getattr result, str() for args.username
- FileContext.py: verbose_default bool annotation
- SSHClient.py: cast SSHClient for dynamic import
- lib/App.py: cast ArgumentParser, add return types to inner funcs
- pm/rpm.py, dpkg.py: cast Iterable[Package]
- App.py: cast for self.args.func(), add return types to inner funcs
- BaseCmdPkgRelations.py: cast str for args.delimiter

Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF:IQ4_NL and pi.dev 0.81.1
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-08-09 12:07:37 +00:00

132 lines
4.2 KiB
Python

from __future__ import annotations
import abc
import io
import tarfile
from tarfile import TarFile, TarInfo
from .CopyContext import CopyContext
from .ExecContext import ExecContext
from .log import DEBUG, ERR, log
from typing import TYPE_CHECKING, Any, override
if TYPE_CHECKING:
from .base import StatResult
class TarIo(CopyContext):
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs['chroot'] = False
super().__init__(*args, **kwargs)
def _match(self, path: str, path_filter: list[str]) -> bool:
return path in path_filter
def _filter_tar_file(
self,
blob: bytes,
path_filter: list[str] | None = None,
matched: list[str] | None = None,
) -> bytes:
ret = io.BytesIO()
with tarfile.open(fileobj = ret, mode = 'w') as tf_out:
tf_in = TarFile(fileobj = io.BytesIO(blob))
for info in tf_in.getmembers():
if path_filter is not None and not self._match(info.name, path_filter):
continue
log(DEBUG, f'Adding {info.name}')
if matched is not None:
matched.append(info.name)
buf = tf_in.extractfile(info)
tf_out.addfile(info, buf)
return ret.getvalue()
async def _read_filtered(
self,
path: str,
path_filter: list[str] | None = None,
matched: list[str] | None = None,
) -> bytes:
try:
blob = (await self.src.get(path)).stdout
except Exception as e:
log(ERR, f'Failed to read tar file "{path}" ({str(e)}')
raise
return self._filter_tar_file(blob, path_filter, matched = matched)
def _add(self, tf: TarFile, path: str, st: StatResult, contents: bytes) -> None:
file_obj = io.BytesIO(contents)
info = TarInfo()
info.name = path
info.mode = st.mode
info.uname = st.owner
info.gname = st.group
info.size = st.size
info.mtime = int(st.mtime)
tf.addfile(info, file_obj)
@abc.abstractmethod
async def _extract(self, blob: bytes, root: str | None = None) -> None:
raise NotImplementedError()
async def extract(
self,
root: str | None = None,
path_filter: list[str] | None = None
) -> list[str]:
ret: list[str] = []
filtered = await self._read_filtered(self.src.root, path_filter, matched = ret)
await self._extract(blob = filtered, root = root)
return ret
@classmethod
def create(cls, *args: Any, type: str | None = None, **kwargs: Any) -> 'TarIo':
if type is not None:
raise NotImplementedError
# return TarIoTarFile(*args, **kwargs)
return TarIoTarExec(*args, **kwargs)
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():
log(DEBUG, f'Extracting {info.name}')
path = root + '/' + info.name if root else info.name
buf = tf.extractfile(info)
if buf is None:
if info.isdir():
await self.dst.mkdir(path, info.mode)
await self.dst.chown(path, info.uname, info.gname)
continue
raise Exception(f'Can\'t extract unsupported file type of "{path}"')
await self.dst.put(
path,
buf.read(),
owner = info.uname,
group = info.gname,
mode = info.mode,
)
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):
raise Exception(
'Tried to get executable destination context from copy '
'context, which only has a file context'
)
return ret