diff --git a/src/borg/item.pyi b/src/borg/item.pyi index f4f5ee3a06..cf731cbe7a 100644 --- a/src/borg/item.pyi +++ b/src/borg/item.pyi @@ -1,4 +1,4 @@ -from collections.abc import Callable, Iterator, Mapping +from collections.abc import Callable, Iterator from typing import Any, NamedTuple from .helpers import StableDict @@ -21,6 +21,17 @@ class PropDict: def __contains__(self, key: str) -> bool: ... def __eq__(self, other: object) -> bool: ... +class PropDictProperty: + key: str + value_type: type + def __init__( + self, + value_type: type, + value_type_name: str | None = ..., + encode: Callable | None = ..., + decode: Callable | None = ..., + ) -> None: ... + class ArchiveItem(PropDict): @property def version(self) -> int: ... @@ -75,17 +86,29 @@ class ArchiveItem(PropDict): @recreate_cmdline.setter def recreate_cmdline(self, val: list[str]) -> None: ... @property - def recreate_args(self) -> Any: ... + def command_line(self) -> str: ... + @command_line.setter + def command_line(self, val: str) -> None: ... + @property + def recreate_command_line(self) -> str: ... + @recreate_command_line.setter + def recreate_command_line(self, val: str) -> None: ... + @property + def recreate_args(self) -> list[str]: ... @recreate_args.setter - def recreate_args(self, val: Any) -> None: ... + def recreate_args(self, val: list[str]) -> None: ... @property - def recreate_partial_chunks(self) -> Any: ... + def recreate_partial_chunks(self) -> list: ... @recreate_partial_chunks.setter - def recreate_partial_chunks(self, val: Any) -> None: ... + def recreate_partial_chunks(self, val: list) -> None: ... @property - def recreate_source_id(self) -> Any: ... + def recreate_source_id(self) -> bytes: ... @recreate_source_id.setter - def recreate_source_id(self, val: Any) -> None: ... + def recreate_source_id(self, val: bytes) -> None: ... + @property + def cwd(self) -> str: ... + @cwd.setter + def cwd(self, val: str) -> None: ... @property def nfiles(self) -> int: ... @nfiles.setter @@ -103,10 +126,6 @@ class ArchiveItem(PropDict): @size_parts.setter def size_parts(self, val: int) -> None: ... @property - def csize(self) -> int: ... - @csize.setter - def csize(self, val: int) -> None: ... - @property def items(self) -> list: ... @items.setter def items(self, val: list) -> None: ... @@ -128,6 +147,10 @@ class Item(PropDict): def source(self) -> str: ... @source.setter def source(self, val: str) -> None: ... + @property + def target(self) -> str: ... + @target.setter + def target(self, val: str) -> None: ... def is_dir(self) -> bool: ... def is_link(self) -> bool: ... def _is_type(self, typetest: Callable) -> bool: ... @@ -250,9 +273,9 @@ class ManifestItem(PropDict): @timestamp.setter def timestamp(self, val: str) -> None: ... @property - def archives(self) -> Mapping[bytes, dict]: ... + def archives(self) -> dict[str, dict]: ... @archives.setter - def archives(self, val: Mapping[bytes, dict]) -> None: ... + def archives(self, val: dict[str, dict]) -> None: ... @property def config(self) -> dict: ... @config.setter @@ -313,13 +336,9 @@ class Key(PropDict): @tam_required.setter def tam_required(self, val: bool) -> None: ... @property - def enc_hmac_key(self) -> bytes: ... - @enc_hmac_key.setter - def enc_hmac_key(self, val: bytes) -> None: ... - @property - def enc_key(self) -> bytes: ... - @enc_key.setter - def enc_key(self, val: bytes) -> None: ... + def crypt_key(self) -> bytes: ... + @crypt_key.setter + def crypt_key(self, val: bytes) -> None: ... @property def id_key(self) -> bytes: ... @id_key.setter @@ -355,6 +374,22 @@ class EncryptedKey(PropDict): @hash.setter def hash(self, val: bytes) -> None: ... @property + def argon2_time_cost(self) -> int: ... + @argon2_time_cost.setter + def argon2_time_cost(self, val: int) -> None: ... + @property + def argon2_memory_cost(self) -> int: ... + @argon2_memory_cost.setter + def argon2_memory_cost(self, val: int) -> None: ... + @property + def argon2_parallelism(self) -> int: ... + @argon2_parallelism.setter + def argon2_parallelism(self, val: int) -> None: ... + @property + def argon2_type(self) -> str: ... + @argon2_type.setter + def argon2_type(self, val: str) -> None: ... + @property def label(self) -> str: ... @label.setter def label(self, val: str) -> None: ... diff --git a/src/borg/testsuite/item_test.py b/src/borg/testsuite/item_test.py index 044796a72c..e517132520 100644 --- a/src/borg/testsuite/item_test.py +++ b/src/borg/testsuite/item_test.py @@ -1,7 +1,12 @@ +import ast +from pathlib import Path + import pytest +from .. import item as item_module from ..cache import ChunkListEntry from ..item import MAX_ALIGN_CHUNKS, MAX_ALIGN_WORK, Item, ItemDiff, chunks_contents_equal, chunks_diff_size +from ..item import PropDict, PropDictProperty from ..helpers import StableDict from ..helpers.msgpack import Timestamp from ..platformflags import is_pypy @@ -234,3 +239,35 @@ def test_chunks_diff_size_over_work_limit(): assert chunks_diff_size(chunks1, chunks2) == (0, 0) # a chunk that really was added is still counted correctly. assert chunks_diff_size(chunks1, chunks2 + [CC]) == (10, 0) + + +def stub_properties(): + """Return {class name: {property name: {"get": annotation, "set": annotation}}} as declared in item.pyi.""" + stub = ast.parse(Path(item_module.__file__).with_name("item.pyi").read_text()) + classes = {} + for cls in stub.body: + if not isinstance(cls, ast.ClassDef): + continue + props = classes[cls.name] = {} + for func in cls.body: + if not isinstance(func, ast.FunctionDef): + continue + for decorator in func.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == "property": + props.setdefault(func.name, {})["get"] = ast.unparse(func.returns) + elif isinstance(decorator, ast.Attribute) and decorator.attr == "setter": + props.setdefault(func.name, {})["set"] = ast.unparse(func.args.args[1].annotation) + return classes + + +@pytest.mark.parametrize("cls", PropDict.__subclasses__(), ids=lambda cls: cls.__name__) +def test_stub_matches_propdict_properties(cls): + """item.pyi is maintained by hand, so check that it agrees with the PropDictProperty table in item.pyx.""" + stub_props = stub_properties()[cls.__name__] + props = {name: value for name, value in vars(cls).items() if isinstance(value, PropDictProperty)} + assert sorted(stub_props) == sorted(props) + for name, prop in props.items(): + getter, setter = stub_props[name].get("get"), stub_props[name].get("set") + assert getter == setter, f"{cls.__name__}.{name}: getter and setter annotations differ" + # compare the base type only, e.g. "list[str]" -> "list". + assert getter.split("[")[0] == prop.value_type.__name__, f"{cls.__name__}.{name}: wrong type"