diff --git a/ci-constraints-requirements.txt b/ci-constraints-requirements.txt index a803acda21ef..e87ddaceecd9 100644 --- a/ci-constraints-requirements.txt +++ b/ci-constraints-requirements.txt @@ -248,6 +248,8 @@ tomli==2.4.1 ; python_full_version <= '3.11' # nox # pytest # sphinx +ty==0.0.69 + # via cryptography (pyproject.toml:dev) typing-extensions==4.16.0 # via # cryptography (pyproject.toml) diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index 147de318e40f..a2f2fcf0cfe6 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -24,6 +24,22 @@ running ``ruff`` against it. If you've installed the development requirements this will automatically use our configuration. You can also run the ``nox`` job with ``nox -e flake``. +We type check with both ``mypy`` and ``ty``. The two use different suppression +comments, so a line that needs suppressing in both must carry both, with the +``mypy`` one first (``mypy`` only recognizes ``# type: ignore`` when it is the +first comment on the line): + +.. code-block:: python + + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] + +``ty`` does not understand ``mypy``'s error codes (see `ty#3127`_) and ignores +a ``# type: ignore[...]`` comment that has any codes in brackets, so a +``mypy``-only suppression needs nothing extra. A bare ``# type: ignore``, on +the other hand, is honored by both, and needs no ``ty`` comment. + +Don't add a ``# ty: ignore`` that isn't needed: ``ty`` reports unused ones. + `Write comments as complete sentences.`_ Class names which contains acronyms or initialisms should always be @@ -148,3 +164,4 @@ So, specifically: .. _`syntax`: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists .. _`Studies have shown`: https://smartbear.com/learn/code-review/best-practices-for-peer-code-review/ .. _`our mailing list`: https://mail.python.org/mailman/listinfo/cryptography-dev +.. _`ty#3127`: https://github.com/astral-sh/ty/issues/3127 diff --git a/noxfile.py b/noxfile.py index 3b6069166fc3..61a5d28d0e71 100644 --- a/noxfile.py +++ b/noxfile.py @@ -54,6 +54,26 @@ def load_pyproject_toml() -> dict: return tomllib.load(f) +def run_ty(session: nox.Session) -> None: + # ty rejects every `pytest.skip("...")` under pytest 8, the newest pytest + # supporting 3.9 (https://github.com/astral-sh/ty/issues/2797). + if sys.version_info < (3, 10): + session.log("Skipping ty: needs pytest 9, which needs Python 3.10+") + return + + session.run( + "ty", + "check", + # ty targets `project.requires-python`, not the running interpreter. + f"--python-version={sys.version_info[0]}.{sys.version_info[1]}", + "src/cryptography/", + "vectors/cryptography_vectors/", + "tests/", + "release.py", + "noxfile.py", + ) + + @nox.session @nox.session(name="tests-ssh") @nox.session(name="tests-randomorder") @@ -232,6 +252,7 @@ def flake(session: nox.Session) -> None: "release.py", "noxfile.py", ) + run_ty(session) session.run("check-sdist", "--no-isolation") @@ -323,14 +344,8 @@ def local(session: nox.Session): external=True, ) - session.run( - "mypy", - "src/cryptography/", - "vectors/cryptography_vectors/", - "tests/", - "release.py", - "noxfile.py", - ) + # `local` runs ty instead of mypy; `flake` runs both. + run_ty(session) session.run( "maturin", diff --git a/pyproject.toml b/pyproject.toml index b34ba7debd05..dcf6dbde0cd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ sdist = ["build >=1.0.0"] pep8test = [ "ruff >=0.11.11", "mypy >=1.14", + "ty >=0.0.69", "check-sdist", "click >=8.0.1", ] @@ -159,6 +160,21 @@ warn_unused_configs = true strict_equality = true strict_bytes = true +# ty does not understand mypy's error codes, so a line both checkers flag +# carries a `# type: ignore[...]` followed by a `# ty: ignore[...]`. If ty +# grows support for mypy's names (https://github.com/astral-sh/ty/issues/3127) +# the `ty:` halves can be dropped. +[tool.ty.rules] +# In `sys.version_info`-gated code one branch is always unreachable, and ty +# reports the blanket ignore there as unused while mypy stays quiet. Whichever +# version we check, one arm of such a pair gets flagged. mypy's +# `warn_unused_ignores` already covers `# type: ignore` comments, so turn this +# off. `unused-ignore-comment` (for `# ty: ignore`) stays enabled, since mypy +# can't check those. +# Tracked upstream as https://github.com/astral-sh/ty/issues/2681; this can be +# re-enabled once ty stops reporting suppressions in unreachable code. +unused-type-ignore-comment = "ignore" + [[tool.mypy.overrides]] module = ["pretend"] ignore_missing_imports = true diff --git a/src/cryptography/hazmat/bindings/openssl/binding.py b/src/cryptography/hazmat/bindings/openssl/binding.py index 6e13df166a9d..937648d6fc90 100644 --- a/src/cryptography/hazmat/bindings/openssl/binding.py +++ b/src/cryptography/hazmat/bindings/openssl/binding.py @@ -36,7 +36,7 @@ def build_conditional_library( conditional_names: Mapping[str, Callable[[], list[str]]], ) -> typing.Any: conditional_lib = types.ModuleType("lib") - conditional_lib._original_lib = lib # type: ignore[attr-defined] + conditional_lib._original_lib = lib # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] excluded_names = set() for condition, names_cb in conditional_names.items(): if not getattr(lib, condition): diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index dc7728e23e67..8d8de2e1eeda 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -2510,7 +2510,7 @@ def __init__(self, oid: ObjectIdentifier, value: bytes) -> None: self._value = value @property - def oid(self) -> ObjectIdentifier: # type: ignore[override] + def oid(self) -> ObjectIdentifier: # type: ignore[override] # ty: ignore[invalid-attribute-override] return self._oid @property diff --git a/tests/hazmat/asn1/test_api.py b/tests/hazmat/asn1/test_api.py index 916f66877183..d22216a23f59 100644 --- a/tests/hazmat/asn1/test_api.py +++ b/tests/hazmat/asn1/test_api.py @@ -225,7 +225,7 @@ class Example: with pytest.raises( TypeError, match="got an unexpected keyword argument 'bar'" ): - Example(bar=3) # type: ignore[call-arg] + Example(bar=3) # type: ignore[call-arg] # ty: ignore[missing-argument, unknown-argument] def test_fail_init_missing_field_name(self) -> None: @asn1.sequence @@ -239,7 +239,7 @@ class Example: ) with pytest.raises(TypeError, match=expected_err): - Example() # type: ignore[call-arg] + Example() # type: ignore[call-arg] # ty: ignore[missing-argument] def test_fail_positional_field_initialization(self) -> None: @asn1.sequence @@ -475,7 +475,7 @@ def test_fail_choice_with_non_literal_tag(self) -> None: class Example: foo: typing.Union[ Annotated[ - asn1.Variant[int, str], + asn1.Variant[int, str], # ty: ignore[invalid-type-arguments] asn1.Implicit(0), ], Annotated[ @@ -542,7 +542,7 @@ class Example: with pytest.raises( TypeError, match="got an unexpected keyword argument 'bar'" ): - Example(bar=3) # type: ignore[call-arg] + Example(bar=3) # type: ignore[call-arg] # ty: ignore[missing-argument, unknown-argument] def test_fail_init_missing_field_name(self) -> None: @asn1.set @@ -556,7 +556,7 @@ class Example: ) with pytest.raises(TypeError, match=expected_err): - Example() # type: ignore[call-arg] + Example() # type: ignore[call-arg] # ty: ignore[missing-argument] def test_fail_positional_field_initialization(self) -> None: @asn1.set diff --git a/tests/hazmat/primitives/test_dh.py b/tests/hazmat/primitives/test_dh.py index a9505688ecdf..a661da11ac7b 100644 --- a/tests/hazmat/primitives/test_dh.py +++ b/tests/hazmat/primitives/test_dh.py @@ -506,7 +506,7 @@ def test_public_key_equality(self): assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_public_key_copy(self): key_bytes = load_vectors_from_file( diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py index 037189e236ad..d8ff2e7cb10b 100644 --- a/tests/hazmat/primitives/test_dsa.py +++ b/tests/hazmat/primitives/test_dsa.py @@ -397,7 +397,7 @@ def test_public_key_equality(self): assert key1 != key3 assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_public_key_copy(self): key_bytes = load_vectors_from_file( diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py index 35cd9a367eb6..a5deccb556c9 100644 --- a/tests/hazmat/primitives/test_ec.py +++ b/tests/hazmat/primitives/test_ec.py @@ -763,7 +763,7 @@ def test_public_key_equality(self, backend): assert key1 != key3 assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_public_key_copy(self, backend): _skip_curve_unsupported(backend, ec.SECP256R1()) diff --git a/tests/hazmat/primitives/test_ed25519.py b/tests/hazmat/primitives/test_ed25519.py index c674a09e8f17..32daeaacd055 100644 --- a/tests/hazmat/primitives/test_ed25519.py +++ b/tests/hazmat/primitives/test_ed25519.py @@ -286,7 +286,7 @@ def test_public_key_equality(): assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_public_key_copy(): diff --git a/tests/hazmat/primitives/test_ed448.py b/tests/hazmat/primitives/test_ed448.py index f2ff3f0adf28..4c1006197e70 100644 --- a/tests/hazmat/primitives/test_ed448.py +++ b/tests/hazmat/primitives/test_ed448.py @@ -303,7 +303,7 @@ def test_public_key_equality(): assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] @pytest.mark.supported( diff --git a/tests/hazmat/primitives/test_hkdf.py b/tests/hazmat/primitives/test_hkdf.py index dcc25d21dd40..5fd8e10832ed 100644 --- a/tests/hazmat/primitives/test_hkdf.py +++ b/tests/hazmat/primitives/test_hkdf.py @@ -117,7 +117,7 @@ def test_private_extract_exists(self): # This was DeprecatedIn47 but we can't raise a warning # because the scapy tests are fragile butterflies hkdf = HKDF(hashes.SHA256(), 32, salt=b"0", info=None) - prk = hkdf._extract(b"0") # type:ignore[attr-defined] + prk = hkdf._extract(b"0") # type:ignore[attr-defined] # ty: ignore[unresolved-attribute] assert len(prk) == 32 def test_buffer_protocol(self): diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index b8818f99ffe7..a05d97a94822 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -2817,7 +2817,7 @@ def test_public_key_equality(self, rsa_key_2048: rsa.RSAPrivateKey): assert key1 != key3 assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_public_key_copy(self, rsa_key_2048: rsa.RSAPrivateKey): key1 = rsa_key_2048.public_key() diff --git a/tests/hazmat/primitives/test_x25519.py b/tests/hazmat/primitives/test_x25519.py index 5decda249c3f..e3bd40b50fc4 100644 --- a/tests/hazmat/primitives/test_x25519.py +++ b/tests/hazmat/primitives/test_x25519.py @@ -366,7 +366,7 @@ def test_public_key_equality(): assert key1 != key3 assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] @pytest.mark.supported( diff --git a/tests/hazmat/primitives/test_x448.py b/tests/hazmat/primitives/test_x448.py index 0b4e0ff740a3..7bab13edb876 100644 --- a/tests/hazmat/primitives/test_x448.py +++ b/tests/hazmat/primitives/test_x448.py @@ -291,7 +291,7 @@ def test_public_key_equality(): assert key1 != key3 assert key1 != object() with pytest.raises(TypeError): - key1 < key2 # type: ignore[operator] + key1 < key2 # type: ignore[operator] # ty: ignore[unsupported-operator] @pytest.mark.supported( diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 8b92856af290..bd71066d90ec 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -290,7 +290,7 @@ def aead_exception_test(cipher_factory, mode_factory): with pytest.raises(AlreadyUpdated): decryptor.authenticate_additional_data(b"b" * 16) with pytest.raises(AttributeError): - decryptor.tag # type: ignore[attr-defined] + decryptor.tag # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] def generate_aead_tag_exception_test(cipher_factory, mode_factory): diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index 19bb40152821..58ebc55cf8de 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -316,7 +316,7 @@ def test_comparison(self): x509.load_der_x509_crl, ) with pytest.raises(TypeError): - crl1 < crl1 # type: ignore[operator] + crl1 < crl1 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_update_dates(self): crl = _load_cert( @@ -1585,7 +1585,7 @@ def test_ordering_unsupported(self): x509.load_pem_x509_certificate, ) with pytest.raises(TypeError, match="'>' not supported"): - cert > cert2 # type: ignore[operator] + cert > cert2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_hash(self): cert1 = _load_cert( @@ -2385,7 +2385,7 @@ def test_ordering_unsupported(self): x509.load_pem_x509_csr, ) with pytest.raises(TypeError, match="'>' not supported"): - csr > csr2 # type: ignore[operator] + csr > csr2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_hash(self): request1 = _load_cert( @@ -4921,7 +4921,7 @@ def test_sign_without_private_key(self, rsa_key_2048: rsa.RSAPrivateKey): ) with pytest.raises(TypeError): - builder.sign(None, None) # type:ignore[arg-type] + builder.sign(None, None) # type:ignore[arg-type] # ty: ignore[invalid-argument-type] def test_build_unsigned_cert(self, rsa_key_2048: rsa.RSAPrivateKey): subject_private_key = rsa_key_2048 @@ -6697,7 +6697,7 @@ def test_comparison(self): oid1 = x509.ObjectIdentifier("2.999.1") oid2 = x509.ObjectIdentifier("2.999.2") with pytest.raises(TypeError): - oid1 < oid2 # type: ignore[operator] + oid1 < oid2 # type: ignore[operator] # ty: ignore[unsupported-operator] def test_repr(self): oid = x509.ObjectIdentifier("2.5.4.3")