From f8c76a67581c11e99135a49becf0a94829305c3a Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 13:37:44 +1100 Subject: [PATCH 1/2] chore(pre-commit): add mypy This commit adds mypy to the pre-commit hooks. This is executed through `hatch` as mypy needs to be able to find and parse dependencies in hatch's virtual environment. Signed-off-by: JP-Ellis --- .pre-commit-config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 402e1da723..6e24f44e8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,3 +61,15 @@ repos: hooks: - id: commitizen stages: [commit-msg] + + - repo: local + hooks: + # Mypy is difficult to run pre-commit's isolated environment as it needs + # to be able to find dependencies. + - id: mypy + name: mypy + entry: hatch run mypy + language: system + types: [python] + exclude: ^(pact|tests)/(?!v3/).*\.py$ + stages: [pre-push] From 589204a4452b64fdd12296d237a7713a4110e1c4 Mon Sep 17 00:00:00 2001 From: JP-Ellis Date: Thu, 26 Oct 2023 13:45:11 +1100 Subject: [PATCH 2/2] chore(ffi): add typing The C extension module `_ffi` is now minimally typed with `pyi` files. It provides typing for the `ffi` class which holds a number of utility functions such as `ffi.string`, `ffi.cast`, `ffi.new`, etc. The `lib` class is also annotated within the `pyi` file, but this merely means that the type checking does not complain about calls to `lib.pactffi_*` functions. As part of this commit, the `StringResult` has been refactored to ensure that it works better with the type checker. Signed-off-by: JP-Ellis --- pact/v3/_ffi.pyi | 6 ++++++ pact/v3/ffi.py | 54 +++++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 pact/v3/_ffi.pyi diff --git a/pact/v3/_ffi.pyi b/pact/v3/_ffi.pyi new file mode 100644 index 0000000000..897259dca3 --- /dev/null +++ b/pact/v3/_ffi.pyi @@ -0,0 +1,6 @@ +import ctypes + +import cffi + +lib: ctypes.CDLL +ffi: cffi.FFI diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py index e1426feeca..c2cf575998 100644 --- a/pact/v3/ffi.py +++ b/pact/v3/ffi.py @@ -82,6 +82,7 @@ from __future__ import annotations import gc +import typing import warnings from enum import Enum from typing import TYPE_CHECKING, List @@ -89,6 +90,7 @@ from ._ffi import ffi, lib # type: ignore[import] if TYPE_CHECKING: + import cffi from pathlib import Path # The follow types are classes defined in the Rust code. Ultimately, a Python @@ -530,35 +532,27 @@ def __repr__(self) -> str: return f"PactSpecification.{self.name}" -class _StringResult(Enum): +class StringResult: """ - String Result. - - [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) + String result. """ - FAILED = lib.StringResult_Failed - OK = lib.StringResult_Ok - - def __str__(self) -> str: - """ - Informal string representation of the String Result. + class _StringResult(Enum): """ - return self.name + Internal enum from Pact FFI. - def __repr__(self) -> str: - """ - Information-rich string representation of the String Result. + [Rust `StringResult`](https://docs.rs/pact_ffi/0.4.9/pact_ffi/mock_server/enum.StringResult.html) """ - return f"_StringResultEnum.{self.name}" + FAILED = lib.StringResult_Failed + OK = lib.StringResult_Ok -class StringResult: - """ - String result. - """ + class _StringResultCData: + tag: int + ok: cffi.FFI.CData + failed: cffi.FFI.CData - def __init__(self, cdata: ffi.CData) -> None: + def __init__(self, cdata: cffi.FFI.CData) -> None: """ Initialise a new String Result. @@ -569,7 +563,7 @@ def __init__(self, cdata: ffi.CData) -> None: if ffi.typeof(cdata).cname != "struct StringResult": msg = f"cdata must be a struct StringResult, got {ffi.typeof(cdata).cname}" raise TypeError(msg) - self._cdata: ffi.CData = cdata + self._cdata = typing.cast(StringResult._StringResultCData, cdata) def __str__(self) -> str: """ @@ -588,14 +582,14 @@ def is_failed(self) -> bool: """ Whether the result is an error. """ - return self._cdata.tag == _StringResult.FAILED.value + return self._cdata.tag == StringResult._StringResult.FAILED.value @property def is_ok(self) -> bool: """ Whether the result is ok. """ - return self._cdata.tag == _StringResult.OK.value + return self._cdata.tag == StringResult._StringResult.OK.value @property def text(self) -> str: @@ -603,7 +597,10 @@ def text(self) -> str: The text of the result. """ # The specific `.ok` or `.failed` does not matter. - return ffi.string(self._cdata.ok).decode("utf-8") + s = ffi.string(self._cdata.ok) + if isinstance(s, bytes): + return s.decode("utf-8") + return s def raise_exception(self) -> None: """ @@ -625,7 +622,10 @@ def version() -> str: Returns: The version of the pact_ffi library as a string, in the form of `x.y.z`. """ - return ffi.string(lib.pactffi_version()).decode("utf-8") + v = ffi.string(lib.pactffi_version()) + if isinstance(v, bytes): + return v.decode("utf-8") + return v def init(log_env_var: str) -> None: @@ -845,7 +845,9 @@ def get_error_message(length: int = 1024) -> str | None: if ret >= 0: # While the documentation says that the return value is the number of bytes # written, the actually return value is always 0 on success. - if msg := ffi.string(buffer).decode("utf-8"): + if msg := ffi.string(buffer): + if isinstance(msg, bytes): + return msg.decode("utf-8") return msg return None if ret == -1: