-
Notifications
You must be signed in to change notification settings - Fork 230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Annotations for nacl.bindings.crypto_box
#706
Annotations for nacl.bindings.crypto_box
#706
Conversation
This introduced a few errors in nacl.public which I wasn't sure how to suppress. AFAICS both will raise a TypeError at run-time, so I suppressed mypy's errors. I wasn't completely satisfied with this; I very much welcome input here! Box._shared_key =============== The first error mypy reported was ``` src/nacl/public.py:193: error: Incompatible types in assignment (expression has type "None", variable has type "bytes") [assignment] ``` referring to `self._shared_key`. I resolved this by marking `_shared_key: Optional[bytes]` to cover both cases in the constructor. But doing this introduced two new errors: ``` src/nacl/public.py:236: error: Argument 3 to "crypto_box_afternm" has incompatible type "Optional[bytes]"; expected "bytes" [arg-type] src/nacl/public.py:278: error: Argument 3 to "crypto_box_open_afternm" has incompatible type "Optional[bytes]"; expected "bytes" [arg-type] ``` Which correspond to the annotations I added to `crypto_box_afternm` and `crypto_box_open_afternm`'s `k` argument. Both docstrings say that `k` should be a `bytes` instance, and go on to inspect `len(k)`. Therefore, calling this with `k=None` will raise a `TypeError`. SealedBox._private_key ====================== Similarly, I also now have the complaint ``` src/nacl/public.py:382: error: Argument 3 to "crypto_box_seal_open" has incompatible type "Optional[bytes]"; expected "bytes" [arg-type ``` because `self._private_key` may be `None`. In this instance there is an explicit `ensure` check in `crypto_box_seal_open` which will raise a TypeError. I also adjusted the SealedBox docstring to match its `__init__` method, which appears to be the convention (judging by the other classes in this file).
I also noticed that I think the intention is that one either creates a box by providing both keys to |
Am I right in thinking that Not sure I have a good suggestion on how to make that work correctly. Maybe adding support for a |
src/nacl/public.py
Outdated
# Type ignore: self._private_key is an Optional[bytes], but | ||
# crypto_box_seal_open expects it to be a bytes object. It's safe to | ||
# ignore this: if it is None, crypto_box_seal_open will raise a | ||
# TypeError. This is enforced by | ||
# test_sealed_box_public_key_cannot_decrypt. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is a legitimate error and we could be more explicit about it by checking at the start of the function:
if self._private_key is None:
raise TypeError("Must be initialised with a PrivateKey.")
Or similar... I don't think allowing the lower TypeError
to propogate is very clear what has been done wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wanted to provide much better type safety though, then this class could be refactored into 2 clases, or a Generic
class, with this method only existing on the PrivateKey
version.
e.g. Maybe something along these lines (totally untested):
_Key = TypeVar("_Key", PrivateKey, PublicKey)
class SealedBox(Generic[_Key]):
def __init__(self, recipient_key: _Key): ...
def encrypt(self: SealedBox[PrivateKey], ...): ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's separate initial type hinting from changing PyNaCl's API to work better in a type hinted world. The latter is desirable and we should do it where appropriate, but for the moment we're looking for full type hinting of existing surface area (with whatever drawbacks that surface area has).
I'm okay with either the type check @Dreamsorcerer proposes (although you'll need to add a test for that case) or a simple assert self._private_key is not None
. The latter isn't actually safe since you can turn off runtime assertions, but that's no worse than the current API and teaches mypy what the expected type is at that point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would also be possible to change the classmethod to use cls.__new__(cls)
and remove the allowance for None
in __init__
. The risk of that approach is that if other logic is added to __init__
in the future then the classmethod won't get that for free, but I'm okay with that if we want to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's separate initial type hinting from changing PyNaCl's API to work better in a type hinted world.
Agreed: I'm trying to avoid touching the logic and machinery where possible.
I'm okay with either the type check @Dreamsorcerer proposes (although you'll need to add a test for that case)
I think test_sealed_box_public_key_cannot_decrypt
covers this already---do you agree?
I'd still like to add a more helpful exception message while I'm here. Library style seem to be to use ensure(...)
, but I don't think mypy will understand the guarantees it enforces without help from a plugin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also like the use of Generic
, but maybe that's best saved for when we come to tackle public.py
proper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't resist. It does spot the following error:
SealedBox(PublicKey(b"publickey")).decrypt(b"ciphertext")
src/nacl/public.py:404: error: Invalid self argument "SealedBox[PublicKey]" to attribute function "decrypt" with type "Callable[[SealedBox[PrivateKey], bytes, Type[_Encoder]], Any]" [misc]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Library style seem to be to use ensure(...), but I don't think mypy will understand the guarantees it enforces without help from a plugin.
Yep, it won't work with mypy. It also seems less readable and longer code than just doing it the standard way, so I'd suggest ditching that function personally.
_Box = TypeVar("_Box", bound="Box") | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have been added in the following commit; apologies for the poor hygiene here.
* Reorder subpackages above top-level modules * Annotations for nacl.hash * Annotations for nacl.hashlib * annotations for nacl.secret * annotations for nacl.public Had some fun merging in the changes from #692 with #706. I didn't go all-in to make `PrivateKey`'s classmethods take a generic `cls` parameter. Felt it was easier to keep things simple. * Annotations for nacl.signing * Tidy up mypy config Now that all files are annotated, apply common settings globally so that the overrides only apply to `nacl.bindings`. In doing so, account for an `Any`-expression in `scrypt.py`. I must not have correctly configured mypy for `nacl.pwhash` in #718: I think it should have been `nacl.pwhash.*` instead of `nacl.pwhash`. * Include a PEP 561 `py.typed` marker file
This introduced a few errors in nacl.public which I wasn't sure how to
suppress. AFAICS both will raise a TypeError at run-time, so I
suppressed mypy's errors. I wasn't completely satisfied with this; I
very much welcome input here! (cc @Dreamsorcerer)
Box._shared_key
The first error mypy reported was
referring to
self._shared_key
. I resolved this by marking_shared_key: Optional[bytes]
to cover both cases in the constructor.But doing this introduced two new errors:
Which correspond to the annotations I added to
crypto_box_afternm
andcrypto_box_open_afternm
'sk
argument. Both docstrings say thatk
should be a
bytes
instance, and go on to inspectlen(k)
. Therefore,calling this with
k=None
will raise aTypeError
.SealedBox._private_key
The second of the original two errors was
because
self._private_key
may beNone
. In this instance there is anexplicit
ensure
check incrypto_box_seal_open
which will raise aTypeError.
I also adjusted the SealedBox docstring to match its
__init__
method,which appears to be the convention (judging by the other classes in this
file).