Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 55 additions & 20 deletions src/borg/item.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: ...
Expand Down Expand Up @@ -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
Expand All @@ -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: ...
Expand All @@ -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: ...
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: ...
37 changes: 37 additions & 0 deletions src/borg/testsuite/item_test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Loading