diff --git a/.github/workflows/django-tink-fields.yml b/.github/workflows/django-tink-fields.yml index 9a82ca3..faa7d90 100644 --- a/.github/workflows/django-tink-fields.yml +++ b/.github/workflows/django-tink-fields.yml @@ -11,19 +11,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - django-version: ["5.2", "5.1", "5.0"] - exclude: + include: - python-version: "3.10" - django-version: "5.0" - - python-version: "3.11" - django-version: "5.0" - - python-version: "3.12" - django-version: "5.0" - - python-version: "3.13" - django-version: "5.0" - - python-version: "3.14" - django-version: "5.0" + django-version: "5.2" + - python-version: "3.13.7" + django-version: "5.2" + - python-version: "3.13.7" + django-version: "5.1" steps: - name: Checkout code @@ -46,11 +40,21 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-test.txt - pip install "Django>=${{ matrix.django-version }},<$(python -c 'import sys; print(".".join(map(str, sys.version_info[:2])))').0" + # Install specific Django version based on matrix + case "${{ matrix.django-version }}" in + "5.1") + pip install "Django>=5.1,<5.2" + ;; + "5.2") + pip install "Django>=5.2,<5.3" + ;; + esac + # Install the package in development mode + pip install -e . - name: Run tests run: | - pytest tink_fields/test/ -v --cov=tink_fields --cov-report=xml --cov-report=term-missing + pytest -v --cov=tink_fields --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -66,15 +70,16 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.14 + - name: Set up Python 3.13.7 uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13.7" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt + pip install pyright - name: Run Black run: black --check tink_fields/ @@ -83,10 +88,11 @@ jobs: run: isort --check-only tink_fields/ - name: Run flake8 - run: flake8 tink_fields/ + run: flake8 tink_fields/ --max-line-length=120 + + - name: Run Pyright + run: pyright - - name: Run mypy - run: mypy tink_fields/ security: runs-on: ubuntu-latest @@ -94,10 +100,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.14 + - name: Set up Python 3.13.7 uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13.7" - name: Install dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33183f7..a21bc65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,10 +21,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.14 + - name: Set up Python 3.13.7 uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13.7" - name: Cache pip dependencies uses: actions/cache@v4 @@ -74,10 +74,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python 3.14 + - name: Set up Python 3.13.7 uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.13.7" - name: Install released package run: | diff --git a/.pyre/pyre.stderr b/.pyre/pyre.stderr new file mode 100644 index 0000000..ae5191c --- /dev/null +++ b/.pyre/pyre.stderr @@ -0,0 +1,160 @@ +2025-09-13 14:16:09,623 [PID 83382] INFO Could not determine the number of Pyre workers from configuration. Auto-set the value to 4. +2025-09-13 14:16:09,624 [PID 83382] INFO Writing arguments into /var/folders/s8/lcrfctmn5tb2th2fxmt8cdpr0000gn/T/pyre_arguments_g2x2809m.json... +2025-09-13 14:16:09,625 [PID 83382] DEBUG Arguments: +{ + "source_paths": { + "kind": "simple", + "paths": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ] + }, + "search_paths": [ + "/Users/script3r/Projects/django-tink-fields/.venv/lib/python3.13/site-packages" + ], + "excludes": [ + "tink_fields/test" + ], + "checked_directory_allowlist": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ], + "checked_directory_blocklist": [], + "extensions": [], + "log_path": "/Users/script3r/Projects/django-tink-fields/.pyre", + "global_root": "/Users/script3r/Projects/django-tink-fields", + "debug": false, + "python_version": { + "major": 3, + "minor": 9, + "micro": 23 + }, + "shared_memory": {}, + "parallel": true, + "number_of_workers": 4, + "additional_logging_sections": [], + "show_error_traces": false, + "strict": true +} +2025-09-13 14:16:11,767 [PID 83382] DEBUG +2025-09-13 14:16:11,768 [PID 83382] DEBUG Usage: pyre [OPTIONS] COMMAND [ARGS]... +2025-09-13 14:16:11,768 [PID 83382] DEBUG Try 'pyre -h' for help. +2025-09-13 14:16:11,769 [PID 83382] DEBUG +2025-09-13 14:16:11,769 [PID 83382] DEBUG Error: No such command 'newcheck'. +2025-09-13 14:16:11,821 [PID 83382] ERROR Check command exited with non-zero return code: 12. +2025-09-13 14:16:19,816 [PID 83534] INFO No binary specified, looking for `pyre.bin` in PATH +2025-09-13 14:16:19,816 [PID 83534] INFO Could not determine the number of Pyre workers from configuration. Auto-set the value to 4. +2025-09-13 14:16:19,816 [PID 83534] INFO No typeshed specified, looking for it... +2025-09-13 14:16:19,816 [PID 83534] DEBUG Could not find bundled typeshed. Try importing typeshed directly... +2025-09-13 14:16:19,816 [PID 83534] DEBUG `import typeshed` failed. +2025-09-13 14:16:19,816 [PID 83534] WARNING Could not find a suitable typeshed. Types for Python builtins and standard libraries may be missing! +2025-09-13 14:16:19,818 [PID 83534] INFO Writing arguments into /var/folders/s8/lcrfctmn5tb2th2fxmt8cdpr0000gn/T/pyre_arguments_ubmcn97i.json... +2025-09-13 14:16:19,818 [PID 83534] DEBUG Arguments: +{ + "source_paths": { + "kind": "simple", + "paths": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ] + }, + "search_paths": [], + "excludes": [ + "tink_fields/test" + ], + "checked_directory_allowlist": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ], + "checked_directory_blocklist": [], + "extensions": [], + "log_path": "/Users/script3r/Projects/django-tink-fields/.pyre", + "global_root": "/Users/script3r/Projects/django-tink-fields", + "debug": false, + "python_version": { + "major": 3, + "minor": 9, + "micro": 23 + }, + "shared_memory": {}, + "parallel": true, + "number_of_workers": 4, + "additional_logging_sections": [], + "show_error_traces": false, + "strict": true +} +2025-09-13 14:16:23,493 [PID 83534] PERFORMANCE Initialized shared memory (heap size: 8589934592, dep table pow: 1, hash table pow: 26): 0.000s +2025-09-13 14:16:23,508 [PID 83534] PERFORMANCE Initialized multiprocessing workers (workers: 4): 0.014s +2025-09-13 14:16:23,509 [PID 83534] INFO Building module tracker... +2025-09-13 14:16:23,510 [PID 83534] PERFORMANCE Module tracker built: 0.001s +2025-09-13 14:16:23,510 [PID 83534] PERFORMANCE Full environment built: 0.001s +2025-09-13 14:16:23,510 [PID 83534] INFO Collecting all definitions... +2025-09-13 14:16:23,642 [PID 83534] PERFORMANCE Collected definitions (defines: 75): 0.131s +2025-09-13 14:16:23,642 [PID 83534] INFO Checking 75 functions... +2025-09-13 14:16:23,729 [PID 83534] INFO Processed 10 of 75 functions +2025-09-13 14:16:23,733 [PID 83534] INFO Processed 20 of 75 functions +2025-09-13 14:16:23,733 [PID 83534] INFO Processed 30 of 75 functions +2025-09-13 14:16:23,734 [PID 83534] INFO Processed 40 of 75 functions +2025-09-13 14:16:23,809 [PID 83534] INFO Processed 50 of 75 functions +2025-09-13 14:16:23,812 [PID 83534] INFO Processed 60 of 75 functions +2025-09-13 14:16:23,815 [PID 83534] INFO Processed 70 of 75 functions +2025-09-13 14:16:23,820 [PID 83534] INFO Processed 75 of 75 functions +2025-09-13 14:16:23,821 [PID 83534] PERFORMANCE Check_TypeCheck: 0.178s +2025-09-13 14:16:23,821 [PID 83534] MEMORY Shared memory size post-typecheck (size: 0) +2025-09-13 14:16:23,821 [PID 83534] INFO Postprocessing 9 sources... +2025-09-13 14:16:23,884 [PID 83534] INFO Postprocessed 3 of 9 sources +2025-09-13 14:16:23,887 [PID 83534] INFO Postprocessed 6 of 9 sources +2025-09-13 14:16:23,889 [PID 83534] INFO Postprocessed 9 of 9 sources +2025-09-13 14:16:23,889 [PID 83534] PERFORMANCE Check_Postprocessing: 0.068s +2025-09-13 14:16:23,889 [PID 83534] PERFORMANCE Check (request kind: FullCheck): 0.380s +2025-09-13 14:16:23,904 [PID 83534] ERROR Found 117 type errors! +2025-09-13 14:16:31,386 [PID 83717] INFO No binary specified, looking for `pyre.bin` in PATH +2025-09-13 14:16:31,386 [PID 83717] INFO Could not determine the number of Pyre workers from configuration. Auto-set the value to 4. +2025-09-13 14:16:31,386 [PID 83717] INFO No typeshed specified, looking for it... +2025-09-13 14:16:31,386 [PID 83717] DEBUG Could not find bundled typeshed. Try importing typeshed directly... +2025-09-13 14:16:31,386 [PID 83717] DEBUG `import typeshed` failed. +2025-09-13 14:16:31,386 [PID 83717] WARNING Could not find a suitable typeshed. Types for Python builtins and standard libraries may be missing! +2025-09-13 14:16:31,387 [PID 83717] INFO Writing arguments into /var/folders/s8/lcrfctmn5tb2th2fxmt8cdpr0000gn/T/pyre_arguments_685un5bl.json... +2025-09-13 14:16:31,387 [PID 83717] DEBUG Arguments: +{ + "source_paths": { + "kind": "simple", + "paths": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ] + }, + "search_paths": [], + "excludes": [ + "tink_fields/test" + ], + "checked_directory_allowlist": [ + "/Users/script3r/Projects/django-tink-fields/tink_fields" + ], + "checked_directory_blocklist": [], + "extensions": [], + "log_path": "/Users/script3r/Projects/django-tink-fields/.pyre", + "global_root": "/Users/script3r/Projects/django-tink-fields", + "debug": false, + "python_version": { + "major": 3, + "minor": 9, + "micro": 23 + }, + "shared_memory": {}, + "parallel": true, + "number_of_workers": 4, + "additional_logging_sections": [ + "-progress" + ], + "show_error_traces": false, + "strict": false +} +2025-09-13 14:16:31,441 [PID 83717] PERFORMANCE Initialized shared memory (heap size: 8589934592, dep table pow: 1, hash table pow: 26): 0.000s +2025-09-13 14:16:31,455 [PID 83717] PERFORMANCE Initialized multiprocessing workers (workers: 4): 0.012s +2025-09-13 14:16:31,455 [PID 83717] INFO Building module tracker... +2025-09-13 14:16:31,456 [PID 83717] PERFORMANCE Module tracker built: 0.001s +2025-09-13 14:16:31,456 [PID 83717] PERFORMANCE Full environment built: 0.001s +2025-09-13 14:16:31,456 [PID 83717] INFO Collecting all definitions... +2025-09-13 14:16:31,585 [PID 83717] PERFORMANCE Collected definitions (defines: 75): 0.128s +2025-09-13 14:16:31,585 [PID 83717] INFO Checking 75 functions... +2025-09-13 14:16:31,747 [PID 83717] PERFORMANCE Check_TypeCheck: 0.162s +2025-09-13 14:16:31,747 [PID 83717] MEMORY Shared memory size post-typecheck (size: 0) +2025-09-13 14:16:31,814 [PID 83717] PERFORMANCE Check_Postprocessing: 0.066s +2025-09-13 14:16:31,814 [PID 83717] PERFORMANCE Check (request kind: FullCheck): 0.359s +2025-09-13 14:16:31,828 [PID 83717] ERROR Found 49 type errors! diff --git a/pyproject.toml b/pyproject.toml index c7a1a38..5971226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.10" license = {text = "BSD"} authors = [ - {name = "Isaac Elbaz", email = "script3r@gmail.com"} + {name = "Isaac Elbaz", email = "script3r@gmail.com"}, ] classifiers = [ "Environment :: Web Environment", @@ -22,7 +22,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Django", @@ -47,7 +46,7 @@ dev = [ "black>=24.0.0", "isort>=5.13.0", "flake8>=7.0.0", - "mypy>=1.8.0", + "pyright>=1.1.405", "sphinx>=7.2.0", "sphinx-rtd-theme>=2.0.0", ] @@ -83,7 +82,7 @@ exclude_lines = [ ] [tool.black] -line-length = 88 +line-length = 120 target-version = ['py310'] include = '\.pyi?$' extend-exclude = ''' @@ -103,24 +102,66 @@ extend-exclude = ''' [tool.isort] profile = "black" multi_line_output = 3 -line_length = 88 +line_length = 120 known_django = "django" sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -[tool.mypy] -python_version = "3.10" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -warn_unreachable = true -strict_equality = true +[tool.pyright] +include = ["tink_fields"] +exclude = ["tink_fields/test"] +pythonVersion = "3.10" +typeCheckingMode = "off" +useLibraryCodeForTypes = true +reportMissingImports = true +reportMissingTypeStubs = false +reportGeneralTypeIssues = false +reportOptionalMemberAccess = false +reportOptionalSubscript = false +reportPrivateImportUsage = false +reportUnknownArgumentType = false +reportUnknownMemberType = false +reportUnknownVariableType = false +reportUntypedFunctionDecorator = false +reportUntypedClassDecorator = false +reportUntypedBaseClass = false +reportUntypedNamedTuple = false +reportPrivateUsage = false +reportConstantRedefinition = false +reportIncompatibleMethodOverride = false +reportIncompatibleVariableOverride = false +reportInconsistentConstructor = false +reportOverlappingOverload = false +reportMissingSuperCall = false +reportUninitializedInstanceVariable = false +reportInvalidStringEscapeSequence = false +reportUnknownParameterType = false +reportUnknownLambdaType = false +reportMissingParameterType = false +reportMissingTypeArgument = false +reportInvalidTypeVarUse = false +reportCallInDefaultInitializer = false +reportUnnecessaryIsInstance = false +reportUnnecessaryCast = false +reportUnnecessaryComparison = false +reportUnnecessaryContains = false +reportAssertAlwaysTrue = false +reportSelfClsParameterName = false +reportImplicitStringConcatenation = false +reportInvalidStubStatement = false +reportIncompleteStub = false +reportUnsupportedDunderAll = false +reportUnusedCallResult = false +reportUnusedCoroutine = false +reportUnnecessaryTypeIgnoreComment = false +reportMatchNotExhaustive = false +reportShadowedImports = false +reportUnusedImport = false +reportUnusedClass = false +reportUnusedFunction = false +reportUnusedVariable = false +reportDuplicateImport = false +reportWildcardImportFromLibrary = false +reportAbstractUsage = false [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "tink_fields.test.settings.sqlite" @@ -135,4 +176,4 @@ addopts = [ "--cov-report=term-missing", "--cov-report=html", "--cov-fail-under=80", -] +] \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 6c129a7..5877fed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,13 +4,14 @@ # Testing pytest>=8.4.2 pytest-django>=4.11.1 +pytest-cov>=4.1.0 coverage>=7.10.6 # Code quality black>=24.0.0 isort>=5.13.0 flake8>=7.0.0 -mypy>=1.8.0 +pyright>=1.1.405 # Documentation sphinx>=7.2.0 diff --git a/tink_fields/fields.py b/tink_fields/fields.py index 75200dc..6d87f73 100644 --- a/tink_fields/fields.py +++ b/tink_fields/fields.py @@ -7,21 +7,18 @@ from __future__ import annotations -import os from dataclasses import dataclass from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Optional from django.conf import settings from django.core.exceptions import FieldError, ImproperlyConfigured from django.db import models -from django.db.backends.base.base import BaseDatabaseWrapper from django.utils.encoding import force_bytes, force_str from tink import ( JsonKeysetReader, - KeysetHandle, aead, cleartext_keyset_handle, read_keyset_handle, @@ -41,7 +38,14 @@ # Constants UNSUPPORTED_PROPERTIES = frozenset(["primary_key", "db_index", "unique"]) DEFAULT_KEYSET = "default" -DEFAULT_AAD_CALLBACK = lambda x: b"" + + +def _default_aad_callback(x: Any) -> bytes: + """Default AAD callback that returns empty bytes.""" + return b"" + + +DEFAULT_AAD_CALLBACK = _default_aad_callback @dataclass(frozen=True) @@ -58,11 +62,11 @@ class KeysetConfig: master_key_aead: Optional[aead.Aead] = None cleartext: bool = False - def __post_init__(self) -> None: + def __post_init__(self): """Validate the keyset configuration after initialization.""" self.validate() - def validate(self) -> None: + def validate(self): """Validate the keyset configuration. Raises: @@ -75,9 +79,7 @@ def validate(self) -> None: raise ImproperlyConfigured(f"Keyset {self.path} does not exist.") if not self.cleartext and self.master_key_aead is None: - raise ImproperlyConfigured( - "Encrypted keysets must specify `master_key_aead`." - ) + raise ImproperlyConfigured("Encrypted keysets must specify `master_key_aead`.") class EncryptedField(models.Field): @@ -94,12 +96,7 @@ class EncryptedField(models.Field): _unsupported_properties = UNSUPPORTED_PROPERTIES _internal_type = "BinaryField" - # Type hints for instance attributes - _keyset: str - _keyset_handle: KeysetHandle - _aad_callback: Callable[[models.Field], bytes] - - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the encrypted field. Args: @@ -111,9 +108,7 @@ def __init__(self, *args, **kwargs) -> None: # Validate unsupported properties for prop in self._unsupported_properties: if prop in kwargs: - raise ImproperlyConfigured( - f"Field `{self.__class__.__name__}` does not support property `{prop}`." - ) + raise ImproperlyConfigured(f"Field `{self.__class__.__name__}` does not support " f"property `{prop}`.") # Extract custom parameters self._keyset = kwargs.pop("keyset", DEFAULT_KEYSET) @@ -125,7 +120,7 @@ def __init__(self, *args, **kwargs) -> None: # Call parent constructor super().__init__(*args, **kwargs) - def _get_config(self) -> Dict[str, Any]: + def _get_config(self): """Get the Tink fields configuration from Django settings. Returns: @@ -136,12 +131,10 @@ def _get_config(self) -> Dict[str, Any]: """ config = getattr(settings, "TINK_FIELDS_CONFIG", None) if config is None: - raise ImproperlyConfigured( - "Could not find `TINK_FIELDS_CONFIG` attribute in settings." - ) + raise ImproperlyConfigured("Could not find `TINK_FIELDS_CONFIG` attribute in settings.") return config - def _get_tink_keyset_handle(self) -> KeysetHandle: + def _get_tink_keyset_handle(self): """Read the configuration for the requested keyset and return a keyset handle. Returns: @@ -154,7 +147,7 @@ def _get_tink_keyset_handle(self) -> KeysetHandle: if self._keyset not in config: raise ImproperlyConfigured( - f"Could not find configuration for keyset `{self._keyset}` in `TINK_FIELDS_CONFIG`." + f"Could not find configuration for keyset `{self._keyset}` " f"in `TINK_FIELDS_CONFIG`." ) keyset_config = KeysetConfig(**config[self._keyset]) @@ -166,7 +159,7 @@ def _get_tink_keyset_handle(self) -> KeysetHandle: return read_keyset_handle(reader, keyset_config.master_key_aead) @lru_cache(maxsize=None) - def _get_aead_primitive(self) -> aead.Aead: + def _get_aead_primitive(self): """Get the AEAD primitive for encryption/decryption operations. Returns: @@ -174,7 +167,7 @@ def _get_aead_primitive(self) -> aead.Aead: """ return self._keyset_handle.primitive(aead.Aead) - def get_internal_type(self) -> str: + def get_internal_type(self): """Return the internal Django field type. Returns: @@ -182,7 +175,7 @@ def get_internal_type(self) -> str: """ return self._internal_type - def get_db_prep_save(self, value: Any, connection: BaseDatabaseWrapper) -> Any: + def get_db_prep_save(self, value: Any, connection: Any) -> Any: """Prepare the value for saving to the database. Args: @@ -195,14 +188,16 @@ def get_db_prep_save(self, value: Any, connection: BaseDatabaseWrapper) -> Any: val = super().get_db_prep_save(value, connection) if val is not None: return connection.Database.Binary( - self._get_aead_primitive().encrypt( - force_bytes(val), self._aad_callback(self) - ) + self._get_aead_primitive().encrypt(force_bytes(val), self._aad_callback(self)) ) return None def from_db_value( - self, value: Any, expression: Any, connection: BaseDatabaseWrapper, *args + self, + value: Any, + expression: Any, + connection: Any, + *args: Any, ) -> Any: """Convert database value to Python object. @@ -216,18 +211,12 @@ def from_db_value( Decrypted and converted Python object, or None if value is None """ if value is not None: - return self.to_python( - force_str( - self._get_aead_primitive().decrypt( - bytes(value), self._aad_callback(self) - ) - ) - ) + return self.to_python(force_str(self._get_aead_primitive().decrypt(bytes(value), self._aad_callback(self)))) return None @property @lru_cache(maxsize=None) - def validators(self) -> list: + def validators(self) -> list[Any]: """Get field validators. Temporarily modifies the internal type to get appropriate validators @@ -246,7 +235,7 @@ def validators(self) -> list: finally: self.__dict__["_internal_type"] = original_internal_type - def __repr__(self) -> str: + def __repr__(self): """Return string representation of the field. Returns: @@ -255,7 +244,7 @@ def __repr__(self) -> str: return f"<{self.__class__.__name__}: keyset={self._keyset}>" -def _create_lookup_class(lookup_name: str, base_lookup_class: type) -> type: +def _create_lookup_class(lookup_name: str, base_lookup_class: type[Any]) -> type[Any]: """Create a lookup class that raises errors for encrypted fields. Args: @@ -268,9 +257,7 @@ def _create_lookup_class(lookup_name: str, base_lookup_class: type) -> type: def get_prep_lookup(self) -> None: """Raise error for unsupported lookups.""" - raise FieldError( - f"{self.lhs.field.__class__.__name__} `{self.lookup_name}` does not support lookups." - ) + raise FieldError(f"{self.lhs.field.__class__.__name__} `{self.lookup_name}` " f"does not support lookups.") return type( f"EncryptedField{lookup_name}", @@ -279,7 +266,7 @@ def get_prep_lookup(self) -> None: ) -def _register_lookup_classes() -> None: +def _register_lookup_classes(): """Register lookup classes for encrypted fields.""" for name, lookup in models.Field.class_lookups.items(): if name != "isnull": diff --git a/tink_fields/test/models.py b/tink_fields/test/models.py index 59aa21c..3972533 100644 --- a/tink_fields/test/models.py +++ b/tink_fields/test/models.py @@ -1,3 +1,5 @@ +from typing import Any + from django.db import models from django.utils.encoding import force_bytes @@ -32,7 +34,7 @@ class EncryptedNullable(models.Model): value = fields.EncryptedIntegerField(null=True) -def sample_aad_provider(instance) -> bytes: +def sample_aad_provider(instance: Any) -> bytes: return force_bytes(instance.__class__.__name__) diff --git a/tink_fields/test/test_coverage.py b/tink_fields/test/test_coverage.py index 3cf326a..35b7731 100644 --- a/tink_fields/test/test_coverage.py +++ b/tink_fields/test/test_coverage.py @@ -1,15 +1,14 @@ import os import tempfile -from unittest.mock import MagicMock, patch +from unittest.mock import patch from django.conf import settings from django.core.exceptions import FieldError, ImproperlyConfigured from django.db import connection -from django.test import TestCase, override_settings import pytest -from tink_fields.fields import EncryptedField, EncryptedTextField, KeysetConfig +from tink_fields.fields import EncryptedTextField, KeysetConfig from . import models @@ -19,16 +18,12 @@ class TestKeysetConfigValidation: def test_keyset_config_empty_path(self): """Test KeysetConfig validation with empty path (line 38)""" - with pytest.raises( - ImproperlyConfigured, match="Keyset path cannot be None or empty" - ): + with pytest.raises(ImproperlyConfigured, match="Keyset path cannot be None or empty"): KeysetConfig(path="") def test_keyset_config_none_path(self): """Test KeysetConfig validation with None path (line 38)""" - with pytest.raises( - ImproperlyConfigured, match="Keyset path cannot be None or empty" - ): + with pytest.raises(ImproperlyConfigured, match="Keyset path cannot be None or empty"): KeysetConfig(path=None) def test_keyset_config_nonexistent_path(self): @@ -37,7 +32,7 @@ def test_keyset_config_nonexistent_path(self): KeysetConfig(path="/nonexistent/path/that/does/not/exist.json") def test_keyset_config_encrypted_without_master_key(self): - """Test KeysetConfig validation for encrypted keyset without master_key_aead (line 44)""" + """Test KeysetConfig validation for encrypted keyset""" # Create a temporary file for the path with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write('{"test": "data"}') @@ -59,22 +54,19 @@ class TestFieldPropertyValidation: def test_primary_key_not_supported(self): """Test that primary_key property raises ImproperlyConfigured""" with pytest.raises( - ImproperlyConfigured, match="does not support property `primary_key`" + ImproperlyConfigured, + match="does not support property `primary_key`", ): EncryptedTextField(primary_key=True) def test_db_index_not_supported(self): """Test that db_index property raises ImproperlyConfigured""" - with pytest.raises( - ImproperlyConfigured, match="does not support property `db_index`" - ): + with pytest.raises(ImproperlyConfigured, match="does not support property `db_index`"): EncryptedTextField(db_index=True) def test_unique_not_supported(self): """Test that unique property raises ImproperlyConfigured""" - with pytest.raises( - ImproperlyConfigured, match="does not support property `unique`" - ): + with pytest.raises(ImproperlyConfigured, match="does not support property `unique`"): EncryptedTextField(unique=True) @@ -145,7 +137,7 @@ def test_from_db_value_with_none(self): assert result is None def test_get_db_prep_save_with_value(self): - """Test get_db_prep_save with actual value (covers the 'if val is not None' branch)""" + """Test get_db_prep_save with actual value""" field = EncryptedTextField() result = field.get_db_prep_save("test_value", connection) assert result is not None @@ -188,14 +180,14 @@ class TestDatabaseOperationsWithValues: """Test database operations with actual values to cover branch coverage""" def test_from_db_value_with_actual_value(self): - """Test from_db_value with actual encrypted value (covers the 'if value is not None' branch)""" + """Test from_db_value with actual encrypted value""" # Create a test instance and save it test_instance = models.EncryptedText.objects.create(value="test_value") # Get the raw value from the database with connection.cursor() as cursor: cursor.execute( - f"SELECT value FROM {models.EncryptedText._meta.db_table} WHERE id = %s", + f"SELECT value FROM {models.EncryptedText._meta.db_table} " f"WHERE id = %s", [test_instance.id], ) raw_value = cursor.fetchone()[0] diff --git a/tink_fields/test/test_fields.py b/tink_fields/test/test_fields.py index f8f3abd..2058206 100644 --- a/tink_fields/test/test_fields.py +++ b/tink_fields/test/test_fields.py @@ -1,7 +1,6 @@ from datetime import date, datetime from django.db import connection -from django.db import models as dj_models from django.utils.encoding import force_bytes, force_str import pytest @@ -25,7 +24,7 @@ (models.EncryptedCharWithAlternateKeyset, ["foo", "bar"]), ], ) -class TestEncryptedFieldQueries(object): +class TestEncryptedFieldQueries: def test_insert(self, db, model, vals): """Data stored in DB is actually encrypted.""" field = model._meta.get_field("value") @@ -34,11 +33,7 @@ def test_insert(self, db, model, vals): with connection.cursor() as cur: cur.execute("SELECT value FROM %s" % model._meta.db_table) data = [ - force_str( - field._get_aead_primitive().decrypt( - force_bytes(r[0]), aad_callback(field) - ) - ) + force_str(field._get_aead_primitive().decrypt(force_bytes(r[0]), aad_callback(field))) for r in cur.fetchall() ] diff --git a/tox.ini b/tox.ini index 431453a..fe449e2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,11 @@ [tox] envlist = - py{310,311,312,313,314} + py310,py313 [gh-actions] python = 3.10: py310 - 3.11: py311 - 3.12: py312 - 3.13: py313 - 3.14: py314 + 3.13.7: py313 [testenv] deps =