From 4cb54187f9cfd50f5b7fc30f148fd8a6b2239fb1 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 19 Dec 2022 20:30:36 -0500 Subject: [PATCH 1/9] replace the hand-rolled parser with one based on parsec --- setup.cfg | 2 + src/tahoe_capabilities/parser.py | 336 +++++++----------- .../test/test_capabilities.py | 104 +++++- 3 files changed, 242 insertions(+), 200 deletions(-) diff --git a/setup.cfg b/setup.cfg index 667ee44..023190d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,9 +17,11 @@ package_dir = packages = find: install_requires = attrs + parsec [options.extras_require] test = + testtools twisted hypothesis diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index af737e4..7b42dcd 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -1,5 +1,7 @@ from base64 import b32decode as _b32decode -from typing import Callable, Dict, List, TypeVar, cast +from typing import Callable, Dict, List, TypeVar, cast, Optional + +from parsec import Parser, string, many1, many, digit, times, one_of, ParseError from .types import ( Capability, @@ -48,190 +50,34 @@ def _unb32str(s: str) -> bytes: return _b32decode(s.encode("ascii")) -def _parse_chk_verify(pieces: List[str]) -> CHKVerify: - verifykey = _unb32str(pieces[0]) - uri_extension_hash = _unb32str(pieces[1]) - needed = int(pieces[2]) - total = int(pieces[3]) - size = int(pieces[4]) - return CHKVerify(verifykey, uri_extension_hash, needed, total, size) - - -def _parse_chk_read(pieces: List[str]) -> CHKRead: - readkey = _unb32str(pieces[0]) - uri_extension_hash = _unb32str(pieces[1]) - needed = int(pieces[2]) - total = int(pieces[3]) - size = int(pieces[4]) - return CHKRead.derive(readkey, uri_extension_hash, needed, total, size) - - -def _parse_dir2_chk_verify(pieces: List[str]) -> CHKDirectoryVerify: - return CHKDirectoryVerify(_parse_chk_verify(pieces)) - - -def _parse_dir2_chk_read(pieces: List[str]) -> CHKDirectoryRead: - return CHKDirectoryRead(_parse_chk_read(pieces)) - - -def _parse_literal(pieces: List[str]) -> LiteralRead: - return LiteralRead(_unb32str(pieces[0])) - - -def _parse_dir2_literal_read(pieces: List[str]) -> LiteralDirectoryRead: - return LiteralDirectoryRead(_parse_literal(pieces)) - - -def _parse_ssk_verify(pieces: List[str]) -> SSKVerify: - storage_index = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return SSKVerify(storage_index, fingerprint) - - -def _parse_ssk_read(pieces: List[str]) -> SSKRead: - readkey = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return SSKRead.derive(readkey, fingerprint) - - -def _parse_dir2_ssk_verify(pieces: List[str]) -> SSKDirectoryVerify: - return SSKDirectoryVerify(_parse_ssk_verify(pieces)) - - -def _parse_dir2_ssk_read(pieces: List[str]) -> SSKDirectoryRead: - return SSKDirectoryRead(_parse_ssk_read(pieces)) - - -def _parse_mdmf_verify(pieces: List[str]) -> MDMFVerify: - storage_index = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return MDMFVerify(storage_index, fingerprint) - - -def _parse_mdmf_read(pieces: List[str]) -> MDMFRead: - readkey = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return MDMFRead.derive(readkey, fingerprint) - - -def _parse_dir2_mdmf_read(pieces: List[str]) -> MDMFDirectoryRead: - return MDMFDirectoryRead(_parse_mdmf_read(pieces)) - - -def _parse_dir2_mdmf_verify(pieces: List[str]) -> MDMFDirectoryVerify: - return MDMFDirectoryVerify(_parse_mdmf_verify(pieces)) - - -def _parse_ssk_write(pieces: List[str]) -> SSKWrite: - writekey = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return SSKWrite.derive(writekey, fingerprint) - - -def _parse_dir2_ssk_write(pieces: List[str]) -> SSKDirectoryWrite: - return SSKDirectoryWrite(_parse_ssk_write(pieces)) - - -def _parse_mdmf_write(pieces: List[str]) -> MDMFWrite: - writekey = _unb32str(pieces[0]) - fingerprint = _unb32str(pieces[1]) - return MDMFWrite.derive(writekey, fingerprint) - - -def _parse_dir2_mdmf_write(pieces: List[str]) -> MDMFDirectoryWrite: - return MDMFDirectoryWrite(_parse_mdmf_write(pieces)) - - -_parsers: Dict[str, Callable[[List[str]], Capability]] = { - "LIT": _parse_literal, - "CHK-Verifier": _parse_chk_verify, - "CHK": _parse_chk_read, - "SSK-Verifier": _parse_ssk_verify, - "SSK-RO": _parse_ssk_read, - "SSK": _parse_ssk_write, - "MDMF-Verifier": _parse_mdmf_verify, - "MDMF-RO": _parse_mdmf_read, - "MDMF": _parse_mdmf_write, - "DIR2-LIT": _parse_dir2_literal_read, - "DIR2-CHK-Verifier": _parse_dir2_chk_verify, - "DIR2-CHK": _parse_dir2_chk_read, - "DIR2-Verifier": _parse_dir2_ssk_verify, - "DIR2-RO": _parse_dir2_ssk_read, - "DIR2": _parse_dir2_ssk_write, - "DIR2-MDMF-Verifier": _parse_dir2_mdmf_verify, - "DIR2-MDMF-RO": _parse_dir2_mdmf_read, - "DIR2-MDMF": _parse_dir2_mdmf_write, -} - -_A = TypeVar("_A") - - -def _uri_parser(s: str, parsers: Dict[str, Callable[[List[str]], _A]]) -> _A: - pieces = s.split(":") - if pieces[0] == "URI": - try: - parser = parsers[pieces[1]] - except KeyError: - raise NotRecognized(pieces[:2]) - else: - return parser(pieces[2:]) - raise NotRecognized(pieces[:1]) - - def writeable_from_string(s: str) -> WriteCapability: - return cast( - WriteCapability, - _uri_parser( - s, - { - "SSK": _parse_ssk_write, - "MDMF": _parse_mdmf_write, - "DIR2": _parse_dir2_ssk_write, - "DIR2-MDMF": _parse_dir2_mdmf_write, - }, - ), + cap = capability_from_string(s) + assert isinstance( + cap, + (SSKWrite, MDMFWrite, SSKDirectoryWrite, MDMFDirectoryWrite), ) + return cap def readable_from_string(s: str) -> ReadCapability: - return cast( - ReadCapability, - _uri_parser( - s, - { - "LIT": _parse_literal, - "CHK": _parse_chk_read, - "SSK-RO": _parse_ssk_read, - "MDMF-RO": _parse_mdmf_write, - }, - ), + cap = capability_from_string(s) + assert isinstance( + cap, + (SSKRead, MDMFRead, SSKDirectoryRead, MDMFDirectoryRead), ) + return cap def immutable_readonly_from_string(s: str) -> ImmutableReadCapability: - return cast( - ImmutableReadCapability, - _uri_parser( - s, - { - "LIT": _parse_literal, - "CHK": _parse_chk_read, - }, - ), - ) + cap = capability_from_string(s) + assert isinstance(cap, (LiteralRead, CHKRead)) + return cap def immutable_directory_from_string(s: str) -> ImmutableDirectoryReadCapability: - return cast( - ImmutableDirectoryReadCapability, - _uri_parser( - s, - { - "DIR2-LIT": _parse_dir2_literal_read, - "DIR2-CHK": _parse_dir2_chk_read, - }, - ), - ) + cap = capability_from_string(s) + assert isinstance(cap, (LiteralDirectoryRead, CHKDirectoryRead)) + return cap def readonly_directory_from_string(s: str) -> DirectoryReadCapability: @@ -242,16 +88,9 @@ def readonly_directory_from_string(s: str) -> DirectoryReadCapability: :raise ValueError: If the string represents a capability that is not read-only, is not for a mutable, or is not for a directory. """ - return cast( - DirectoryReadCapability, - _uri_parser( - s, - { - "DIR2-RO": _parse_dir2_ssk_read, - "DIR2-MDMF-RO": _parse_dir2_mdmf_read, - }, - ), - ) + cap = capability_from_string(s) + assert isinstance(cap, (SSKDirectoryRead, MDMFDirectoryRead)) + return cap def writeable_directory_from_string(s: str) -> DirectoryWriteCapability: @@ -262,22 +101,123 @@ def writeable_directory_from_string(s: str) -> DirectoryWriteCapability: :raise ValueError: If the string represents a capability that is writeable or is not for a directory. """ - return cast( - DirectoryWriteCapability, - _uri_parser( - s, - { - "DIR2": _parse_dir2_ssk_write, - "DIR2-MDMF": _parse_dir2_mdmf_write, - }, - ), + cap = capability_from_string(s) + assert isinstance(cap, (SSKDirectoryWrite, MDMFDirectoryWrite)) + return cap + + +def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser: + if exact_bits is None: + return many(one_of(alphabet)) + + full, extra = divmod(exact_bits, 5) + stem = times( + one_of(alphabet), + full, + full, + ) + if extra == 0: + return stem + return (stem + one_of("".join(set(_trailing_b32chars(alphabet, extra))))).parsecmap( + lambda xs_x: xs_x[0] + [xs_x[1]] ) -def capability_from_string(s: str) -> Capability: - pieces = s.split(":") - if pieces[0] == "URI": - parser = _parsers[pieces[1]] - return parser(pieces[2:]) +def b32string(exact_bits: Optional[int] = None) -> Parser: + # RFC3548 standard used by Gnutella, Content-Addressable Web, THEX, Bitzi, + # Web-Calculus... + rfc3548_alphabet = "abcdefghijklmnopqrstuvwxyz234567" + + return _b32string(rfc3548_alphabet, exact_bits).parsecmap( + lambda xs: _unb32str("".join(xs)) + ) - raise NotRecognized(pieces[:1]) + +def _trailing_b32chars(alphabet: str, bits: int) -> str: + stem = alphabet[:: 1 << bits] + if bits == 0: + return stem + return stem + _trailing_b32chars(alphabet, bits - 1) + + +_sep = string(":") +_natural = many1(digit()).parsecmap("".join).parsecmap(int) +_key = b32string(128) +_uri_extension_hash = b32string(256) + +_lit = b32string() + +_chk_params = times(_sep >> _natural, 3, 3) +_chk = _key + (_sep >> _uri_extension_hash) + _chk_params + +# Tahoe-LAFS calls the components of SSK "storage_index" and "fingerprint" but +# they are syntactically the same as "key" and "uri_extension_hash" from CHK. +_ssk = _key + (_sep >> _uri_extension_hash) + +# And MDMF is syntactically compatible with SSK +_mdmf = _ssk + + +def _capability_parser() -> Parser: + lit_read = string("LIT:") >> _lit.parsecmap(LiteralRead) + + def chk_glue(f): + def g(values): + ((key, uri_extension_hash), [a, b, c]) = values + return f(key, uri_extension_hash, a, b, c) + + return g + + chk_verify = string("CHK-Verifier:") >> _chk.parsecmap(chk_glue(CHKVerify)) + chk = string("CHK:") >> _chk.parsecmap(chk_glue(CHKRead.derive)) + + ssk_verify = string("SSK-Verifier:") >> _ssk.parsecmap(lambda p: SSKVerify(*p)) + ssk_read = string("SSK-RO:") >> _ssk.parsecmap(lambda p: SSKRead.derive(*p)) + ssk = string("SSK:") >> _ssk.parsecmap(lambda p: SSKWrite.derive(*p)) + + mdmf_verify = string("MDMF-Verifier:") >> _mdmf.parsecmap(lambda p: MDMFVerify(*p)) + mdmf_read = string("MDMF-RO:") >> _mdmf.parsecmap(lambda p: MDMFRead.derive(*p)) + mdmf = string("MDMF:") >> _mdmf.parsecmap(lambda p: MDMFWrite.derive(*p)) + + def dir2(file_parser: Parser, dir_type: type) -> Parser: + return string("DIR2-") >> file_parser.parsecmap(dir_type) + + return string("URI:") >> ( + lit_read + ^ chk_verify + ^ chk + ^ ssk_verify + ^ ssk_read + ^ ssk + ^ mdmf_verify + ^ mdmf_read + ^ mdmf + # Directory variations, starting with ssk which breaks the pattern by + # leaving "SSK-" out. + ^ ( + string("DIR2-Verifier:") + >> _ssk.parsecmap(lambda p: SSKDirectoryVerify(SSKVerify(*p))) + ) + ^ ( + string("DIR2-RO:") + >> _ssk.parsecmap(lambda p: SSKDirectoryRead(SSKRead.derive(*p))) + ) + ^ ( + string("DIR2:") + >> _ssk.parsecmap(lambda p: SSKDirectoryWrite(SSKWrite.derive(*p))) + ) + # # And then the rest + ^ dir2(lit_read, LiteralDirectoryRead) + ^ dir2(chk_verify, CHKDirectoryVerify) + ^ dir2(chk, CHKDirectoryRead) + ^ dir2(mdmf_verify, MDMFDirectoryVerify) + ^ dir2(mdmf_read, MDMFDirectoryRead) + ^ dir2(mdmf, MDMFDirectoryWrite) + ) + + +_capability = _capability_parser() + + +def capability_from_string(s: str) -> Capability: + return _capability.parse(s) diff --git a/src/tahoe_capabilities/test/test_capabilities.py b/src/tahoe_capabilities/test/test_capabilities.py index e85c235..3865979 100644 --- a/src/tahoe_capabilities/test/test_capabilities.py +++ b/src/tahoe_capabilities/test/test_capabilities.py @@ -1,7 +1,11 @@ +from base64 import b32encode as _b32encode from operator import attrgetter -from unittest import TestCase +from testtools import TestCase +from testtools.content import text_content +from testtools.matchers import Equals, raises from hypothesis import assume, given +from hypothesis.strategies import integers, binary from tahoe_capabilities import ( Capability, @@ -10,11 +14,103 @@ digested_capability_string, ) from tahoe_capabilities.strategies import capabilities +from tahoe_capabilities.parser import ( + _sep, + _natural, + _key, + _lit, + _chk_params, + _chk, + ParseError, +) + + +def b32encode(bs: bytes) -> str: + return _b32encode(bs).lower().decode("ascii").rstrip("=") class ParseTests(TestCase): maxDiff = None + def test_sep(self) -> None: + """ + ``_sep`` parses only ":". + """ + self.expectThat(_sep.parse(":"), Equals(":")) + self.expectThat(lambda: _sep.parse("x"), raises(ParseError)) + + @given(integers(min_value=0)) + def test_natural(self, n) -> None: + """ + ``_natural`` parses non-negative integers. + """ + self.assertThat(_natural.parse(str(n)), Equals(n)) + + def test_natural_fail(self) -> None: + """ + ``_natural`` rejects strings that contain non-digits. + """ + self.assertThat(lambda: _natural.parse("hello"), raises(ParseError)) + self.assertThat(lambda: _natural.parse("-1"), raises(ParseError)) + + @given(binary(min_size=16, max_size=16)) + def test_key(self, bs) -> None: + """ + ``_key`` parses base32-encoded 128 bit strings. + """ + self.assertThat( + _key.parse(b32encode(bs)), + Equals(bs), + ) + + @given(binary(max_size=15)) + def test_key_fail(self, bs) -> None: + """ + ``_key`` rejects strings shorter than 128 bits. + """ + self.assertThat( + lambda: _key.parse(b32encode(bs)), + raises(ParseError), + ) + + @given(binary()) + def test_lit(self, bs) -> None: + """ + ``_lit`` parses base32-encoded strings of any length. + """ + self.assertThat( + _lit.parse(b32encode(bs)), + Equals(bs), + ) + + @given(integers(min_value=1), integers(min_value=1), integers(min_value=1)) + def test_chk_params(self, a, b, c) -> None: + """ + ``_chk_params`` parses strings like:: + + :: + """ + self.assertThat( + _chk_params.parse(f":{a}:{b}:{c}"), + Equals([a, b, c]), + ) + + @given( + binary(min_size=16, max_size=16), + binary(min_size=32, max_size=32), + integers(min_value=1), + integers(min_value=1), + integers(min_value=1), + ) + def test_chk(self, key, ueh, a, b, c) -> None: + """ + ``_chk`` parses a key, sep, uri hash extension, and chk parmaeters. + """ + self.assertThat( + _chk.parse(f"{b32encode(key)}:" f"{b32encode(ueh)}:" f"{a}:{b}:{c}"), + Equals(((key, ueh), [a, b, c])), + ) + @given(capabilities()) def test_from_string_roundtrip(self, cap: Capability) -> None: """ @@ -22,6 +118,7 @@ def test_from_string_roundtrip(self, cap: Capability) -> None: ``danger_real_capability_string``. """ cap_str = danger_real_capability_string(cap) + self.addDetail("cap", text_content(cap_str)) cap_parsed = capability_from_string(cap_str) self.assertEqual(cap_parsed, cap) @@ -108,8 +205,11 @@ class VectorTests(TestCase): def test_vector(self) -> None: for index, (description, start, transform, expected) in self.vector: + parsed = capability_from_string(start) + transformed = transform(parsed) + serialized = danger_real_capability_string(transformed) self.assertEqual( - danger_real_capability_string(transform(capability_from_string(start))), + serialized, expected, f"(#{index}) {description}({start}) != {expected}", ) From c459f4ebb925d1e41713309e1e7e10543145f16e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 19 Dec 2022 20:31:25 -0500 Subject: [PATCH 2/9] relock some things Also remove the redundant "test" extra in devShellForVersions call --- flake.lock | 24 ++++++++++++------------ flake.nix | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/flake.lock b/flake.lock index 7f9e5af..4e2f799 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "flake-utils": { "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "type": "github" }, "original": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1659091740, - "narHash": "sha256-aNtnezQfUX1QS/bFno2H5661qIY/Rn7BmHnspuuyI+4=", + "lastModified": 1662635943, + "narHash": "sha256-1OBBlBzZ894or8eHZjyADOMnGH89pPUKYGVVS5rwW/0=", "owner": "DavHau", "repo": "mach-nix", - "rev": "f15ea8677df951cb4fe608945fd98725dcd033b3", + "rev": "65266b5cc867fec2cb6a25409dd7cd12251f6107", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1659526864, - "narHash": "sha256-XFzXrc1+6DZb9hBgHfEzfwylPUSqVFJbQPs8eOgYufU=", + "lastModified": 1665132027, + "narHash": "sha256-zoHPqSQSENt96zTk6Mt1AP+dMNqQDshXKQ4I6MfjP80=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "478f3cbc8448b5852539d785fbfe9a53304133be", + "rev": "9ecc270f02b09b2f6a76b98488554dd842797357", "type": "github" }, "original": { @@ -60,11 +60,11 @@ "pypi-deps-db": { "flake": false, "locked": { - "lastModified": 1659688860, - "narHash": "sha256-b9CjUto5pfKaHVWnxJG7gpRqacJIryChXbN5K0Owo5o=", + "lastModified": 1665260206, + "narHash": "sha256-LukZvKMArRE3+5/kF7/YOypJ6XTKtkYKkgxYpg/pxHA=", "owner": "DavHau", "repo": "pypi-deps-db", - "rev": "a9ceaf65546ed01049467b4357f310cb0e340276", + "rev": "207b45139d020d459c8e2f70409668f1559d3e95", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2a207d6..374b20a 100644 --- a/flake.nix +++ b/flake.nix @@ -65,7 +65,7 @@ devShells = withDefault - (devShellForVersions ["test"] supportedPythonVersions) + (devShellForVersions [] supportedPythonVersions) defaultPythonVersion; }); } From dfad8a4fd916ddf27a9ddd51f5086ffdd1eda1a3 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:08:10 -0500 Subject: [PATCH 3/9] Some annotation and documentation improvements --- src/tahoe_capabilities/parser.py | 77 ++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index 7b42dcd..d9522f5 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -1,3 +1,11 @@ + +from __future__ import annotations + +__all__ = [ + "capability_from_string", + "ParseError", +] + from base64 import b32decode as _b32decode from typing import Callable, Dict, List, TypeVar, cast, Optional @@ -106,7 +114,19 @@ def writeable_directory_from_string(s: str) -> DirectoryWriteCapability: return cap -def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser: +def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser[list[str]]: + """ + Parse a base32-encoded string. + + :param alphabet: The alphabet to use. Must be 32 characters long. + + :exact_bits: See ``b32string``. + + :return: A parser that consumes and returns the matched base32 characters + (still encoded). + """ + assert len(alphabet) == 32 + if exact_bits is None: return many(one_of(alphabet)) @@ -123,7 +143,18 @@ def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser: ) -def b32string(exact_bits: Optional[int] = None) -> Parser: +def b32string(exact_bits: Optional[int] = None) -> Parser[bytes]: + """ + Parse a base32-encoded string. + + :param alphabet: The alphabet to use. Must be 32 characters long. + + :param exact_bits: If ``None`` parse a string of any length. Otherwise, + parse a base32 string that represents an encoded string of exactly + this many bits. + + :return: A parser that consumes and results in the decoded string. + """ # RFC3548 standard used by Gnutella, Content-Addressable Web, THEX, Bitzi, # Web-Calculus... rfc3548_alphabet = "abcdefghijklmnopqrstuvwxyz234567" @@ -134,31 +165,56 @@ def b32string(exact_bits: Optional[int] = None) -> Parser: def _trailing_b32chars(alphabet: str, bits: int) -> str: + """ + Find the part of the base32 alphabet that is required and allowed to + express the given number of bits of encoded data. + + This is used to match the end of a base32 string where the length of the + encoded data is not a multiple of 5 bits (the base32 word size). + """ stem = alphabet[:: 1 << bits] if bits == 0: return stem return stem + _trailing_b32chars(alphabet, bits - 1) +# Match the common separator between components of capability strings. _sep = string(":") + +# Match a natural number _natural = many1(digit()).parsecmap("".join).parsecmap(int) + +# Match a base32-encoded binary string with the length of a key. _key = b32string(128) + +# Match a base32-encoded binary string with the length of a fingerprint or UEB hash. _uri_extension_hash = b32string(256) +# Match the base32-encoded data portion of a literal capability. This is not +# length limited though in practice literals are only used for data of 55 +# bytes or less. _lit = b32string() +# Match the needed shares, total shares, and data size numbers in a CHK +# capability string. _chk_params = times(_sep >> _natural, 3, 3) + +# Match all of the data components (but not the type prefix) of a CHK +# capability string. _chk = _key + (_sep >> _uri_extension_hash) + _chk_params +# Match all of the data components (but not the type prefix) of an SSK (SDMF +# or MDMF) capability string. +# # Tahoe-LAFS calls the components of SSK "storage_index" and "fingerprint" but # they are syntactically the same as "key" and "uri_extension_hash" from CHK. _ssk = _key + (_sep >> _uri_extension_hash) -# And MDMF is syntactically compatible with SSK -_mdmf = _ssk - -def _capability_parser() -> Parser: +def _capability_parser() -> Parser[Capability]: + """ + Parse any kind of capability string. + """ lit_read = string("LIT:") >> _lit.parsecmap(LiteralRead) def chk_glue(f): @@ -175,9 +231,9 @@ def g(values): ssk_read = string("SSK-RO:") >> _ssk.parsecmap(lambda p: SSKRead.derive(*p)) ssk = string("SSK:") >> _ssk.parsecmap(lambda p: SSKWrite.derive(*p)) - mdmf_verify = string("MDMF-Verifier:") >> _mdmf.parsecmap(lambda p: MDMFVerify(*p)) - mdmf_read = string("MDMF-RO:") >> _mdmf.parsecmap(lambda p: MDMFRead.derive(*p)) - mdmf = string("MDMF:") >> _mdmf.parsecmap(lambda p: MDMFWrite.derive(*p)) + mdmf_verify = string("MDMF-Verifier:") >> _ssk.parsecmap(lambda p: MDMFVerify(*p)) + mdmf_read = string("MDMF-RO:") >> _ssk.parsecmap(lambda p: MDMFRead.derive(*p)) + mdmf = string("MDMF:") >> _ssk.parsecmap(lambda p: MDMFWrite.derive(*p)) def dir2(file_parser: Parser, dir_type: type) -> Parser: return string("DIR2-") >> file_parser.parsecmap(dir_type) @@ -220,4 +276,7 @@ def dir2(file_parser: Parser, dir_type: type) -> Parser: def capability_from_string(s: str) -> Capability: + """ + Parse any known capability string. + """ return _capability.parse(s) From 3e76e61a3a445695e64515d15dfc00003c63c6fc Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:10:30 -0500 Subject: [PATCH 4/9] A few more annotation improvements --- pyproject.toml | 7 +++ src/tahoe_capabilities/parser.py | 11 +++-- src/tahoe_capabilities/types.py | 76 ++++++++++++++++---------------- 3 files changed, 51 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa7e766..f1c9d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ [tool.isort] profile = "black" multi_line_output = 3 + +[tool.mypy] +python_version = "3.10" + +[[tool.mypy.overrides]] +module = "testtools.*" +ignore_missing_imports = true diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index d9522f5..1ed2a4a 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -7,7 +7,6 @@ ] from base64 import b32decode as _b32decode -from typing import Callable, Dict, List, TypeVar, cast, Optional from parsec import Parser, string, many1, many, digit, times, one_of, ParseError @@ -41,7 +40,7 @@ class NotRecognized(ValueError): - def __init__(self, prefix: List[str]) -> None: + def __init__(self, prefix: list[str]) -> None: super().__init__(f"Unrecognized capability type {prefix}") @@ -62,7 +61,7 @@ def writeable_from_string(s: str) -> WriteCapability: cap = capability_from_string(s) assert isinstance( cap, - (SSKWrite, MDMFWrite, SSKDirectoryWrite, MDMFDirectoryWrite), + (SSKWrite, MDMFWrite), ) return cap @@ -71,7 +70,7 @@ def readable_from_string(s: str) -> ReadCapability: cap = capability_from_string(s) assert isinstance( cap, - (SSKRead, MDMFRead, SSKDirectoryRead, MDMFDirectoryRead), + (SSKRead, MDMFRead), ) return cap @@ -114,7 +113,7 @@ def writeable_directory_from_string(s: str) -> DirectoryWriteCapability: return cap -def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser[list[str]]: +def _b32string(alphabet: str, exact_bits: None | int = None) -> Parser[list[str]]: """ Parse a base32-encoded string. @@ -143,7 +142,7 @@ def _b32string(alphabet: str, exact_bits: Optional[int] = None) -> Parser[list[s ) -def b32string(exact_bits: Optional[int] = None) -> Parser[bytes]: +def b32string(exact_bits: None | int = None) -> Parser[bytes]: """ Parse a base32-encoded string. diff --git a/src/tahoe_capabilities/types.py b/src/tahoe_capabilities/types.py index 7a58ac5..fa791d8 100644 --- a/src/tahoe_capabilities/types.py +++ b/src/tahoe_capabilities/types.py @@ -1,4 +1,6 @@ -from typing import Tuple, Union +from __future__ import annotations + +from typing import Union from attrs import field, frozen @@ -9,10 +11,10 @@ class LiteralRead: data: bytes prefix: str = "LIT" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.data,) @@ -20,10 +22,10 @@ def secrets(self) -> Tuple[bytes, ...]: class LiteralDirectoryRead: cap_object: LiteralRead prefix: str = "DIR2-LIT" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @@ -37,11 +39,11 @@ class CHKVerify: prefix: str = "CHK-Verifier" @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.storage_index, self.uri_extension_hash) @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return (str(self.needed), str(self.total), str(self.size)) @@ -78,11 +80,11 @@ def size(self) -> int: return self.verifier.size @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.readkey, self.verifier.uri_extension_hash) @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.verifier.suffix @@ -92,11 +94,11 @@ class CHKDirectoryVerify: prefix: str = "DIR2-CHK-Verifier" @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -110,11 +112,11 @@ def verifier(self) -> CHKDirectoryVerify: return CHKDirectoryVerify(self.cap_object.verifier) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -123,10 +125,10 @@ class SSKVerify: storage_index: bytes fingerprint: bytes prefix: str = "SSK-Verifier" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.storage_index, self.fingerprint) @@ -135,7 +137,7 @@ class SSKRead: readkey: bytes = field(repr=False) verifier: SSKVerify prefix: str = "SSK-RO" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @classmethod def derive(cls, readkey: bytes, fingerprint: bytes) -> "SSKRead": @@ -143,7 +145,7 @@ def derive(cls, readkey: bytes, fingerprint: bytes) -> "SSKRead": return SSKRead(readkey, SSKVerify(storage_index, fingerprint)) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.readkey, self.verifier.fingerprint) @@ -152,7 +154,7 @@ class SSKWrite: writekey: bytes = field(repr=False) reader: SSKRead prefix: str = "SSK" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @classmethod def derive(cls, writekey: bytes, fingerprint: bytes) -> "SSKWrite": @@ -160,7 +162,7 @@ def derive(cls, writekey: bytes, fingerprint: bytes) -> "SSKWrite": return SSKWrite(writekey, SSKRead.derive(readkey, fingerprint)) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.writekey, self.reader.verifier.fingerprint) @@ -170,11 +172,11 @@ class SSKDirectoryVerify: prefix: str = "DIR2-Verifier" @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -188,11 +190,11 @@ def verifier(self) -> SSKDirectoryVerify: return SSKDirectoryVerify(self.cap_object.verifier) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -206,11 +208,11 @@ def reader(self) -> SSKDirectoryRead: return SSKDirectoryRead(self.cap_object.reader) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -219,10 +221,10 @@ class MDMFVerify: storage_index: bytes fingerprint: bytes prefix: str = "MDMF-Verifier" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.storage_index, self.fingerprint) @@ -231,7 +233,7 @@ class MDMFRead: readkey: bytes = field(repr=False) verifier: MDMFVerify prefix: str = "MDMF-RO" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @classmethod def derive(cls, readkey: bytes, fingerprint: bytes) -> "MDMFRead": @@ -239,7 +241,7 @@ def derive(cls, readkey: bytes, fingerprint: bytes) -> "MDMFRead": return MDMFRead(readkey, MDMFVerify(storage_index, fingerprint)) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.readkey, self.verifier.fingerprint) @@ -248,7 +250,7 @@ class MDMFWrite: writekey: bytes = field(repr=False) reader: MDMFRead prefix: str = "MDMF" - suffix: Tuple[str, ...] = field(init=False, default=()) + suffix: tuple[str, ...] = field(init=False, default=()) @classmethod def derive(cls, writekey: bytes, fingerprint: bytes) -> "MDMFWrite": @@ -256,7 +258,7 @@ def derive(cls, writekey: bytes, fingerprint: bytes) -> "MDMFWrite": return MDMFWrite(writekey, MDMFRead.derive(readkey, fingerprint)) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return (self.writekey, self.reader.verifier.fingerprint) @@ -266,11 +268,11 @@ class MDMFDirectoryVerify: prefix: str = "DIR2-MDMF-Verifier" @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -284,11 +286,11 @@ def verifier(self) -> MDMFDirectoryVerify: return MDMFDirectoryVerify(self.cap_object.verifier) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix @@ -302,11 +304,11 @@ def reader(self) -> MDMFDirectoryRead: return MDMFDirectoryRead(self.cap_object.reader) @property - def secrets(self) -> Tuple[bytes, ...]: + def secrets(self) -> tuple[bytes, ...]: return self.cap_object.secrets @property - def suffix(self) -> Tuple[str, ...]: + def suffix(self) -> tuple[str, ...]: return self.cap_object.suffix From e8b8830d84c8dabb61c184c30d036a3c026b2dee Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:13:06 -0500 Subject: [PATCH 5/9] Fix these two to preserve their old runtime behavior The annotations were wrong before but the cast() calls hid the issue. Preserve runtime behavior in case some application is depending on that. --- src/tahoe_capabilities/parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index 1ed2a4a..825f34b 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -57,20 +57,20 @@ def _unb32str(s: str) -> bytes: return _b32decode(s.encode("ascii")) -def writeable_from_string(s: str) -> WriteCapability: +def writeable_from_string(s: str) -> WriteCapability | DirectoryWriteCapability: cap = capability_from_string(s) assert isinstance( cap, - (SSKWrite, MDMFWrite), + (SSKWrite, MDMFWrite, SSKDirectoryWrite, MDMFDirectoryWrite), ) return cap -def readable_from_string(s: str) -> ReadCapability: +def readable_from_string(s: str) -> ReadCapability | DirectoryReadCapability: cap = capability_from_string(s) assert isinstance( cap, - (SSKRead, MDMFRead), + (SSKRead, MDMFRead, LiteralDirectoryRead, CHKDirectoryRead, SSKDirectoryRead, MDMFDirectoryRead), ) return cap From b5b733ad29b00af316b823530aa0766a12bda969 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:15:24 -0500 Subject: [PATCH 6/9] Some more docstrings --- src/tahoe_capabilities/parser.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index 825f34b..54ae828 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -58,6 +58,9 @@ def _unb32str(s: str) -> bytes: def writeable_from_string(s: str) -> WriteCapability | DirectoryWriteCapability: + """ + Parse a capability string into any kind of writeable object. + """ cap = capability_from_string(s) assert isinstance( cap, @@ -67,6 +70,9 @@ def writeable_from_string(s: str) -> WriteCapability | DirectoryWriteCapability: def readable_from_string(s: str) -> ReadCapability | DirectoryReadCapability: + """ + Parse a capability string into any kind of readable object. + """ cap = capability_from_string(s) assert isinstance( cap, @@ -76,12 +82,19 @@ def readable_from_string(s: str) -> ReadCapability | DirectoryReadCapability: def immutable_readonly_from_string(s: str) -> ImmutableReadCapability: + """ + Parse a capability string into any kind of readable, immutable object. + """ cap = capability_from_string(s) assert isinstance(cap, (LiteralRead, CHKRead)) return cap def immutable_directory_from_string(s: str) -> ImmutableDirectoryReadCapability: + """ + Parse a capability string into any kind of readable, immutable + directory. + """ cap = capability_from_string(s) assert isinstance(cap, (LiteralDirectoryRead, CHKDirectoryRead)) return cap From a09c318e5325a59b515355cab46c826dd9ce18fa Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:38:56 -0500 Subject: [PATCH 7/9] Fix a few more mypy complaints Also move the mypy strictness setting from CI to local configuration for easier dev workflow. --- .circleci/config.yml | 2 +- pyproject.toml | 1 + src/tahoe_capabilities/parser.py | 9 ++++++--- src/tahoe_capabilities/test/test_capabilities.py | 16 ++++++++-------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ef0b07..4585351 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -126,7 +126,7 @@ jobs: - "run": name: "mypy" command: | - nix develop --command mypy --strict src + nix develop --command mypy src unittests: parameters: diff --git a/pyproject.toml b/pyproject.toml index f1c9d51..8b5045b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ multi_line_output = 3 [tool.mypy] python_version = "3.10" +strict = true [[tool.mypy.overrides]] module = "testtools.*" diff --git a/src/tahoe_capabilities/parser.py b/src/tahoe_capabilities/parser.py index 54ae828..2bc318b 100644 --- a/src/tahoe_capabilities/parser.py +++ b/src/tahoe_capabilities/parser.py @@ -6,6 +6,7 @@ "ParseError", ] +from typing import Callable, Type, TypeVar from base64 import b32decode as _b32decode from parsec import Parser, string, many1, many, digit, times, one_of, ParseError @@ -38,6 +39,8 @@ WriteCapability, ) +T = TypeVar("T") +S = TypeVar("S") class NotRecognized(ValueError): def __init__(self, prefix: list[str]) -> None: @@ -229,8 +232,8 @@ def _capability_parser() -> Parser[Capability]: """ lit_read = string("LIT:") >> _lit.parsecmap(LiteralRead) - def chk_glue(f): - def g(values): + def chk_glue(f: Callable[[bytes, bytes, int, int, int], T]) -> Callable[[tuple[tuple[bytes, bytes], list[int]]], T]: + def g(values: tuple[tuple[bytes, bytes], list[int]]) -> T: ((key, uri_extension_hash), [a, b, c]) = values return f(key, uri_extension_hash, a, b, c) @@ -247,7 +250,7 @@ def g(values): mdmf_read = string("MDMF-RO:") >> _ssk.parsecmap(lambda p: MDMFRead.derive(*p)) mdmf = string("MDMF:") >> _ssk.parsecmap(lambda p: MDMFWrite.derive(*p)) - def dir2(file_parser: Parser, dir_type: type) -> Parser: + def dir2(file_parser: Parser[T], dir_type: Callable[[T], S]) -> Parser[S]: return string("DIR2-") >> file_parser.parsecmap(dir_type) return string("URI:") >> ( diff --git a/src/tahoe_capabilities/test/test_capabilities.py b/src/tahoe_capabilities/test/test_capabilities.py index 3865979..fb8399c 100644 --- a/src/tahoe_capabilities/test/test_capabilities.py +++ b/src/tahoe_capabilities/test/test_capabilities.py @@ -40,7 +40,7 @@ def test_sep(self) -> None: self.expectThat(lambda: _sep.parse("x"), raises(ParseError)) @given(integers(min_value=0)) - def test_natural(self, n) -> None: + def test_natural(self, n: int) -> None: """ ``_natural`` parses non-negative integers. """ @@ -54,7 +54,7 @@ def test_natural_fail(self) -> None: self.assertThat(lambda: _natural.parse("-1"), raises(ParseError)) @given(binary(min_size=16, max_size=16)) - def test_key(self, bs) -> None: + def test_key(self, bs: bytes) -> None: """ ``_key`` parses base32-encoded 128 bit strings. """ @@ -64,7 +64,7 @@ def test_key(self, bs) -> None: ) @given(binary(max_size=15)) - def test_key_fail(self, bs) -> None: + def test_key_fail(self, bs: bytes) -> None: """ ``_key`` rejects strings shorter than 128 bits. """ @@ -74,7 +74,7 @@ def test_key_fail(self, bs) -> None: ) @given(binary()) - def test_lit(self, bs) -> None: + def test_lit(self, bs: bytes) -> None: """ ``_lit`` parses base32-encoded strings of any length. """ @@ -84,11 +84,11 @@ def test_lit(self, bs) -> None: ) @given(integers(min_value=1), integers(min_value=1), integers(min_value=1)) - def test_chk_params(self, a, b, c) -> None: + def test_chk_params(self, a: int, b: int, c: int) -> None: """ ``_chk_params`` parses strings like:: - :: + ::: """ self.assertThat( _chk_params.parse(f":{a}:{b}:{c}"), @@ -102,12 +102,12 @@ def test_chk_params(self, a, b, c) -> None: integers(min_value=1), integers(min_value=1), ) - def test_chk(self, key, ueh, a, b, c) -> None: + def test_chk(self, key: bytes, ueh: bytes, a: int, b: int, c: int) -> None: """ ``_chk`` parses a key, sep, uri hash extension, and chk parmaeters. """ self.assertThat( - _chk.parse(f"{b32encode(key)}:" f"{b32encode(ueh)}:" f"{a}:{b}:{c}"), + _chk.parse(f"{b32encode(key)}:{b32encode(ueh)}:{a}:{b}:{c}"), Equals(((key, ueh), [a, b, c])), ) From 1b05cf8f0e15c6cb9a6586f5523a0a9a33f66b84 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:43:18 -0500 Subject: [PATCH 8/9] missing docstring --- src/tahoe_capabilities/test/test_capabilities.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tahoe_capabilities/test/test_capabilities.py b/src/tahoe_capabilities/test/test_capabilities.py index fb8399c..40b0234 100644 --- a/src/tahoe_capabilities/test/test_capabilities.py +++ b/src/tahoe_capabilities/test/test_capabilities.py @@ -204,6 +204,11 @@ class VectorTests(TestCase): ) def test_vector(self) -> None: + """ + Certain known-valid capability strings can be parsed, diminished, + and serialized to the correct known-valid diminished capability + strings. + """ for index, (description, start, transform, expected) in self.vector: parsed = capability_from_string(start) transformed = transform(parsed) From 83194637f8b05606b32ac6c32642bb2fd4eb284b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 24 Jan 2023 09:43:22 -0500 Subject: [PATCH 9/9] Quiet a couple mypy errors testtools is unannotated so there's not much we can do about this right here. --- src/tahoe_capabilities/test/test_capabilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tahoe_capabilities/test/test_capabilities.py b/src/tahoe_capabilities/test/test_capabilities.py index 40b0234..18e6248 100644 --- a/src/tahoe_capabilities/test/test_capabilities.py +++ b/src/tahoe_capabilities/test/test_capabilities.py @@ -29,7 +29,7 @@ def b32encode(bs: bytes) -> str: return _b32encode(bs).lower().decode("ascii").rstrip("=") -class ParseTests(TestCase): +class ParseTests(TestCase): # type: ignore[misc] maxDiff = None def test_sep(self) -> None: @@ -161,7 +161,7 @@ def test_digested_capability_string_distinct( reader = attrgetter("reader") -class VectorTests(TestCase): +class VectorTests(TestCase): # type: ignore[misc] """ Test Tahoe-Capabilities behavior on hard-coded values against known-correct test vectors extracted from Tahoe-LAFS.