Repository: jpadilla/pyjwt
Pull request #784 (2022-07-26 17:52:52)

Add type hints to jwt/help.py and add missing types dependency
This merge request:

1. Adds type hints to `jwt/help.py`
2. Adds `types-cryptography` to include types when installing.


Patch:
diff --git a/docs/api.rst b/docs/api.rst
index 2f81b1f..919b6af 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -62,7 +62,7 @@ API Reference
             if ``verify_exp``, ``verify_iat``, and ``verify_nbf`` respectively
             is set to ``True``).
 
-    :param Iterable audience: optional, the value for ``verify_aud`` check
+    :param Union[str, Iterable] audience: optional, the value for ``verify_aud`` check
     :param str issuer: optional, the value for ``verify_iss`` check
     :param float leeway: a time margin in seconds for the expiration check
     :rtype: dict
diff --git a/jwt/api_jwk.py b/jwt/api_jwk.py
index aae2cf1..bbd8a9e 100644
--- a/jwt/api_jwk.py
+++ b/jwt/api_jwk.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import json
 
 from .algorithms import get_default_algorithms
@@ -74,15 +76,15 @@ class PyJWK:
 
 
 class PyJWKSet:
-    def __init__(self, keys):
+    def __init__(self, keys: list[dict]) -> None:
         self.keys = []
 
-        if not keys or not isinstance(keys, list):
-            raise PyJWKSetError("Invalid JWK Set value")
-
-        if len(keys) == 0:
+        if not keys:
             raise PyJWKSetError("The JWK Set did not contain any keys")
 
+        if not isinstance(keys, list):
+            raise PyJWKSetError("Invalid JWK Set value")
+
         for key in keys:
             try:
                 self.keys.append(PyJWK(key))
diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py
index b08e950..91a6d2e 100644
--- a/jwt/api_jwt.py
+++ b/jwt/api_jwt.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import json
 import warnings
 from calendar import timegm
@@ -76,7 +78,7 @@ class PyJWT:
         detached_payload: Optional[bytes] = None,
         # passthrough arguments to _validate_claims
         # consider putting in options
-        audience: Optional[str] = None,
+        audience: Optional[Union[str, Iterable[str]]] = None,
         issuer: Optional[str] = None,
         leeway: Union[int, float, timedelta] = 0,
         # kwargs
@@ -150,7 +152,7 @@ class PyJWT:
         detached_payload: Optional[bytes] = None,
         # passthrough arguments to _validate_claims
         # consider putting in options
-        audience: Optional[str] = None,
+        audience: Optional[Union[str, Iterable[str]]] = None,
         issuer: Optional[str] = None,
         leeway: Union[int, float, timedelta] = 0,
         # kwargs
@@ -180,8 +182,8 @@ class PyJWT:
         if isinstance(leeway, timedelta):
             leeway = leeway.total_seconds()
 
-        if not isinstance(audience, (bytes, str, type(None), Iterable)):
-            raise TypeError("audience must be a string, iterable, or None")
+        if audience is not None and not isinstance(audience, (str, Iterable)):
+            raise TypeError("audience must be a string, iterable or None")
 
         self._validate_required_claims(payload, options)
 
diff --git a/jwt/help.py b/jwt/help.py
index d5c3ebb..767c323 100644
--- a/jwt/help.py
+++ b/jwt/help.py
@@ -1,6 +1,7 @@
 import json
 import platform
 import sys
+from typing import Dict
 
 from . import __version__ as pyjwt_version
 
@@ -10,7 +11,7 @@ except ModuleNotFoundError:
     cryptography = None  # type: ignore
 
 
-def info():
+def info() -> Dict[str, Dict[str, str]]:
     """
     Generate information for a bug report.
     Based on the requests package help utility module.
@@ -29,13 +30,13 @@ def info():
         implementation_version = platform.python_version()
     elif implementation == "PyPy":
         implementation_version = (
-            f"{sys.pypy_version_info.major}."
+            f"{sys.pypy_version_info.major}."  # type: ignore[attr-defined]
             f"{sys.pypy_version_info.minor}."
             f"{sys.pypy_version_info.micro}"
         )
-        if sys.pypy_version_info.releaselevel != "final":
+        if sys.pypy_version_info.releaselevel != "final":  # type: ignore[attr-defined]
             implementation_version = "".join(
-                [implementation_version, sys.pypy_version_info.releaselevel]
+                [implementation_version, sys.pypy_version_info.releaselevel]  # type: ignore[attr-defined]
             )
     else:
         implementation_version = "Unknown"
@@ -51,7 +52,7 @@ def info():
     }
 
 
-def main():
+def main() -> None:
     """Pretty-print the bug information as JSON."""
     print(json.dumps(info(), sort_keys=True, indent=2))
 
diff --git a/setup.cfg b/setup.cfg
index 67d30a7..9fc9169 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -45,6 +45,7 @@ docs =
     zope.interface
 crypto =
     cryptography>=3.3.1
+    types-cryptography>=3.3.21
 tests =
     pytest>=6.0.0,<7.0.0
     coverage[toml]==5.0.4
@@ -53,6 +54,7 @@ dev =
     sphinx-rtd-theme
     zope.interface
     cryptography>=3.3.1
+    types-cryptography>=3.3.21
     pytest>=6.0.0,<7.0.0
     coverage[toml]==5.0.4
     mypy


Test patch:
diff --git a/tests/test_api_jwk.py b/tests/test_api_jwk.py
index 94eb498..040e81b 100644
--- a/tests/test_api_jwk.py
+++ b/tests/test_api_jwk.py
@@ -208,8 +208,8 @@ class TestPyJWK:
             PyJWK.from_dict(v)
 
 
+@crypto_required
 class TestPyJWKSet:
-    @crypto_required
     def test_should_load_keys_from_jwk_data_dict(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -231,7 +231,6 @@ class TestPyJWKSet:
         assert jwk.key_id == "keyid-abc123"
         assert jwk.public_key_use == "sig"
 
-    @crypto_required
     def test_should_load_keys_from_jwk_data_json_string(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -253,7 +252,6 @@ class TestPyJWKSet:
         assert jwk.key_id == "keyid-abc123"
         assert jwk.public_key_use == "sig"
 
-    @crypto_required
     def test_keyset_should_index_by_kid(self):
         algo = RSAAlgorithm(RSAAlgorithm.SHA256)
 
@@ -276,7 +274,6 @@ class TestPyJWKSet:
         with pytest.raises(KeyError):
             _ = jwk_set["this-kid-does-not-exist"]
 
-    @crypto_required
     def test_keyset_with_unknown_alg(self):
         # first keyset with unusable key and usable key
         with open(key_path("jwk_keyset_with_unknown_alg.json")) as keyfile:
@@ -293,3 +290,13 @@ class TestPyJWKSet:
             assert len(jwks.get("keys")) == 1
             with pytest.raises(PyJWKSetError):
                 _ = PyJWKSet.from_json(jwks_text)
+
+    def test_invalid_keys_list(self):
+        with pytest.raises(PyJWKSetError) as err:
+            PyJWKSet(keys="string")
+        assert str(err.value) == "Invalid JWK Set value"
+
+    def test_empty_keys_list(self):
+        with pytest.raises(PyJWKSetError) as err:
+            PyJWKSet(keys=[])
+        assert str(err.value) == "The JWK Set did not contain any keys"
diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py
index d0443e8..bebe7d2 100644
--- a/tests/test_api_jwt.py
+++ b/tests/test_api_jwt.py
@@ -119,7 +119,7 @@ class TestJWT:
             jwt.decode(example_jwt, secret, audience=1, algorithms=["HS256"])
 
         exception = context.value
-        assert str(exception) == "audience must be a string, iterable, or None"
+        assert str(exception) == "audience must be a string, iterable or None"
 
     def test_decode_with_nonlist_aud_claim_throws_exception(self, jwt):
         secret = "secret"
@@ -419,6 +419,14 @@ class TestJWT:
         with pytest.raises(InvalidAudienceError):
             jwt.decode(token, "secret", audience="urn-me", algorithms=["HS256"])
 
+    def test_raise_exception_audience_as_bytes(self, jwt):
+        payload = {"some": "payload", "aud": ["urn:me", "urn:someone-else"]}
+        token = jwt.encode(payload, "secret")
+        with pytest.raises(InvalidAudienceError):
+            jwt.decode(
+                token, "secret", audience="urn:me".encode(), algorithms=["HS256"]
+            )
+
     def test_raise_exception_invalid_audience_in_array(self, jwt):
         payload = {
             "some": "payload",