Skip to content

Commit

Permalink
chore(ffi): add typing
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
JP-Ellis committed Oct 26, 2023
1 parent 678cd00 commit db6374a
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 26 deletions.
6 changes: 6 additions & 0 deletions pact/v3/_ffi.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import ctypes

import cffi

lib: ctypes.CDLL
ffi: cffi.FFI
54 changes: 28 additions & 26 deletions pact/v3/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@
from __future__ import annotations

import gc
import typing
import warnings
from enum import Enum
from typing import TYPE_CHECKING, List

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
Expand Down Expand Up @@ -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.
Expand All @@ -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:
"""
Expand All @@ -588,22 +582,25 @@ 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:
"""
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:
"""
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit db6374a

Please sign in to comment.