From 26a63fc7cd54caa84aef7b4ac30f3f69b0037f6f Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 10 Oct 2024 17:07:33 +0530 Subject: [PATCH] Ruff linter and formatter changes (#1001) * Ruff settings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff toml added in manifest * Ruff formatted files * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Flake8 changes removed * B904 Exception fixes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * B028 Error code changes * B023 exception fixes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * method-assign exception suppressed * method-assign exception suppressed * keywords removed * Mypy error slienced * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Code coverage fixed * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Merge conflicts resolved * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff precommit settings added * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Setup.cfg deleted --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .gitignore | 1 + .pre-commit-config.yaml | 16 ++++---- MANIFEST.in | 1 + jwt/algorithms.py | 34 ++++++++++------- jwt/api_jws.py | 4 +- jwt/api_jwt.py | 15 ++++++-- jwt/help.py | 5 ++- jwt/jwks_client.py | 6 ++- pyproject.toml | 4 ++ ruff.toml | 77 +++++++++++++++++++++++++++++++++++++++ tests/keys/jwk_empty.json | 0 tests/test_algorithms.py | 7 ++++ tests/test_api_jwt.py | 2 +- 13 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 ruff.toml create mode 100644 tests/keys/jwk_empty.json diff --git a/.gitignore b/.gitignore index 6333fe1e..1ab5db68 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ target/ .pytest_cache .mypy_cache pip-wheel-metadata/ +.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08adca4f..ded4567e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,12 +11,6 @@ repos: - id: blacken-docs args: ["--target-version=py38"] - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 - language_version: python3.9 - - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: @@ -62,4 +56,12 @@ repos: hooks: - id: pyroma -... + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.6.9 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/MANIFEST.in b/MANIFEST.in index 949042ce..a0950e96 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include CHANGELOG.rst include LICENSE include README.rst include SECURITY.md +include ruff.toml include tox.ini include jwt/py.typed graft docs diff --git a/jwt/algorithms.py b/jwt/algorithms.py index 9be50b20..a23c88c6 100644 --- a/jwt/algorithms.py +++ b/jwt/algorithms.py @@ -297,7 +297,7 @@ def from_jwk(jwk: str | JWKDict) -> bytes: else: raise ValueError except ValueError: - raise InvalidKeyError("Key is not valid JSON") + raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "oct": raise InvalidKeyError("Not an HMAC key") @@ -346,7 +346,9 @@ def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: try: return cast(RSAPublicKey, load_pem_public_key(key_bytes)) except (ValueError, UnsupportedAlgorithm): - raise InvalidKeyError("Could not parse the provided public key.") + raise InvalidKeyError( + "Could not parse the provided public key." + ) from None @overload @staticmethod @@ -409,10 +411,10 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: else: raise ValueError except ValueError: - raise InvalidKeyError("Key is not valid JSON") + raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "RSA": - raise InvalidKeyError("Not an RSA key") + raise InvalidKeyError("Not an RSA key") from None if "d" in obj and "e" in obj and "n" in obj: # Private key @@ -428,7 +430,7 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: if any_props_found and not all(props_found): raise InvalidKeyError( "RSA key must include all parameters if any are present besides d" - ) + ) from None public_numbers = RSAPublicNumbers( from_base64url_uint(obj["e"]), @@ -520,7 +522,7 @@ def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: ): raise InvalidKeyError( "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms" - ) + ) from None return crypto_key @@ -605,13 +607,13 @@ def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: else: raise ValueError except ValueError: - raise InvalidKeyError("Key is not valid JSON") + raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "EC": - raise InvalidKeyError("Not an Elliptic curve key") + raise InvalidKeyError("Not an Elliptic curve key") from None if "x" not in obj or "y" not in obj: - raise InvalidKeyError("Not an Elliptic curve key") + raise InvalidKeyError("Not an Elliptic curve key") from None x = base64url_decode(obj.get("x")) y = base64url_decode(obj.get("y")) @@ -623,17 +625,23 @@ def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: if len(x) == len(y) == 32: curve_obj = SECP256R1() else: - raise InvalidKeyError("Coords should be 32 bytes for curve P-256") + raise InvalidKeyError( + "Coords should be 32 bytes for curve P-256" + ) from None elif curve == "P-384": if len(x) == len(y) == 48: curve_obj = SECP384R1() else: - raise InvalidKeyError("Coords should be 48 bytes for curve P-384") + raise InvalidKeyError( + "Coords should be 48 bytes for curve P-384" + ) from None elif curve == "P-521": if len(x) == len(y) == 66: curve_obj = SECP521R1() else: - raise InvalidKeyError("Coords should be 66 bytes for curve P-521") + raise InvalidKeyError( + "Coords should be 66 bytes for curve P-521" + ) from None elif curve == "secp256k1": if len(x) == len(y) == 32: curve_obj = SECP256K1() @@ -834,7 +842,7 @@ def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys: else: raise ValueError except ValueError: - raise InvalidKeyError("Key is not valid JSON") + raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "OKP": raise InvalidKeyError("Not an Octet Key Pair") diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 7089d9d9..654ee0b7 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -194,6 +194,7 @@ def decode_complete( "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, + stacklevel=2, ) if options is None: options = {} @@ -239,6 +240,7 @@ def decode( "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, + stacklevel=2, ) decoded = self.decode_complete( jwt, key, algorithms, options, detached_payload=detached_payload @@ -307,7 +309,7 @@ def _verify_signature( try: alg = header["alg"] except KeyError: - raise InvalidAlgorithmError("Algorithm not specified") + raise InvalidAlgorithmError("Algorithm not specified") from None if not alg or (algorithms is not None and alg not in algorithms): raise InvalidAlgorithmError("The specified alg value is not allowed") diff --git a/jwt/api_jwt.py b/jwt/api_jwt.py index 91ad266f..c82e6371 100644 --- a/jwt/api_jwt.py +++ b/jwt/api_jwt.py @@ -122,6 +122,7 @@ def decode_complete( "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, + stacklevel=2, ) options = dict(options or {}) # shallow-copy or initialize an empty dict options.setdefault("verify_signature", True) @@ -135,6 +136,7 @@ def decode_complete( "The equivalent is setting `verify_signature` to False in the `options` dictionary. " "This invocation has a mismatch between the kwarg and the option entry.", category=DeprecationWarning, + stacklevel=2, ) if not options["verify_signature"]: @@ -173,7 +175,7 @@ def _decode_payload(self, decoded: dict[str, Any]) -> Any: try: payload = json.loads(decoded["payload"]) except ValueError as e: - raise DecodeError(f"Invalid payload string: {e}") + raise DecodeError(f"Invalid payload string: {e}") from e if not isinstance(payload, dict): raise DecodeError("Invalid payload string: must be a json object") return payload @@ -202,6 +204,7 @@ def decode( "and will be removed in pyjwt version 3. " f"Unsupported kwargs: {tuple(kwargs.keys())}", RemovedInPyjwt3Warning, + stacklevel=2, ) decoded = self.decode_complete( jwt, @@ -269,7 +272,9 @@ def _validate_iat( try: iat = int(payload["iat"]) except ValueError: - raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.") + raise InvalidIssuedAtError( + "Issued At claim (iat) must be an integer." + ) from None if iat > (now + leeway): raise ImmatureSignatureError("The token is not yet valid (iat)") @@ -282,7 +287,7 @@ def _validate_nbf( try: nbf = int(payload["nbf"]) except ValueError: - raise DecodeError("Not Before claim (nbf) must be an integer.") + raise DecodeError("Not Before claim (nbf) must be an integer.") from None if nbf > (now + leeway): raise ImmatureSignatureError("The token is not yet valid (nbf)") @@ -296,7 +301,9 @@ def _validate_exp( try: exp = int(payload["exp"]) except ValueError: - raise DecodeError("Expiration Time claim (exp) must be an integer.") + raise DecodeError( + "Expiration Time claim (exp) must be an integer." + ) from None if exp <= (now - leeway): raise ExpiredSignatureError("Signature has expired") diff --git a/jwt/help.py b/jwt/help.py index 80b0ca56..8e1c2286 100644 --- a/jwt/help.py +++ b/jwt/help.py @@ -39,7 +39,10 @@ def info() -> Dict[str, Dict[str, str]]: ) if pypy_version_info.releaselevel != "final": implementation_version = "".join( - [implementation_version, pypy_version_info.releaselevel] + [ + implementation_version, + pypy_version_info.releaselevel, + ] ) else: implementation_version = "Unknown" diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index f19b10ac..9a8992ca 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -45,7 +45,9 @@ def __init__( if cache_keys: # Cache signing keys # Ignore mypy (https://github.com/python/mypy/issues/2427) - self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore + self.get_signing_key = lru_cache(maxsize=max_cached_keys)( + self.get_signing_key + ) # type: ignore def fetch_data(self) -> Any: jwk_set: Any = None @@ -58,7 +60,7 @@ def fetch_data(self) -> Any: except (URLError, TimeoutError) as e: raise PyJWKClientConnectionError( f'Fail to fetch data from the url, err: "{e}"' - ) + ) from e else: return jwk_set finally: diff --git a/pyproject.toml b/pyproject.toml index f5821fd7..7462024f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,10 @@ profile = "black" [tool.mypy] allow_incomplete_defs = true allow_untyped_defs = true +disable_error_code = [ + "method-assign", + "unused-ignore", +] ignore_missing_imports = true no_implicit_optional = true overrides = [ diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..255b6f8b --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py38" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F", "B"] +ignore = ["E501"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/tests/keys/jwk_empty.json b/tests/keys/jwk_empty.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 337de96a..35f9cc22 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -108,6 +108,13 @@ def test_hmac_from_jwk_should_raise_exception_if_not_hmac_key(self): with pytest.raises(InvalidKeyError): algo.from_jwk(keyfile.read()) + def test_hmac_from_jwk_should_raise_exception_if_empty_json(self): + algo = HMACAlgorithm(HMACAlgorithm.SHA256) + + with open(key_path("jwk_empty.json")) as keyfile: + with pytest.raises(InvalidKeyError): + algo.from_jwk(keyfile.read()) + @crypto_required def test_rsa_should_parse_pem_public_key(self): algo = RSAAlgorithm(RSAAlgorithm.SHA256) diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py index 509f788f..3c0a975b 100644 --- a/tests/test_api_jwt.py +++ b/tests/test_api_jwt.py @@ -165,7 +165,7 @@ def test_encode_bad_type(self, jwt): for t in types: pytest.raises( TypeError, - lambda: jwt.encode(t, "secret", algorithms=["HS256"]), + lambda t=t: jwt.encode(t, "secret", algorithms=["HS256"]), ) def test_encode_with_typ(self, jwt):