Skip to content

Commit

Permalink
ci: add mypy test
Browse files Browse the repository at this point in the history
Start small with checks for just some files
Solve the problem with missing type hints for the cffi generated
extension by handcrafting the type hints for ffi and generate the
type hints for lib.

Signed-off-by: Erik Larsson <who+github@cnackers.org>
  • Loading branch information
whooo committed Jan 17, 2024
1 parent f2d2a8a commit 49f20a7
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 49 deletions.
7 changes: 7 additions & 0 deletions .ci/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ function run_style() {
"${PYTHON}" -m black --diff --check "${SRC_ROOT}"
}

function run_mypy() {
python3 -m pip install --user -e .[dev]
mypy --strict src/tpm2_pytss
}

if [ "x${TEST}" != "x" ]; then
run_test
elif [ "x${WHITESPACE}" != "x" ]; then
Expand All @@ -111,4 +116,6 @@ elif [ "x${STYLE}" != "x" ]; then
run_style
elif [ "x${PUBLISH_PKG}" != "x" ]; then
run_publish_pkg
elif [ "x${MYPY}" != "x" ]; then
run_mypy
fi
22 changes: 22 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,28 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: /tmp/coverage.xml

mypy:
runs-on: ubuntu-20.04

steps:
- name: Checkout Repository
uses: actions/checkout@v2

- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: 3.x

- name: Install dependencies
env:
TPM2_TSS_VERSION: 4.0.1
run: ./.ci/install-deps.sh

- name: Check
env:
MYPY: 1
run: ./.ci/run.sh

whitespace-check:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ htmlcov
/.pytest_cache/
src/tpm2_pytss/internal/type_mapping.py
src/tpm2_pytss/internal/versions.py
src/tpm2_pytss/_libtpm2_pytss/lib.pyi
22 changes: 21 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3", "pycparser", "pkgconfig"]
requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3", "pycparser", "pkgconfig", "cffi"]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]
Expand All @@ -19,6 +19,26 @@ exclude = '''
| build
| dist
| esys_binding.py
| .*\.pyi$
)
)
'''

[tool.mypy]
mypy_path = "mypy_stubs"
exclude = [
'src/tpm2_pytss/utils.py',
'src/tpm2_pytss/internal/templates.py',
'src/tpm2_pytss/encoding.py',
'src/tpm2_pytss/tsskey.py',
'src/tpm2_pytss/policy.py',
'src/tpm2_pytss/ESAPI.py',
'src/tpm2_pytss/FAPI.py',
'src/tpm2_pytss/types.py',
'src/tpm2_pytss/internal/crypto.py',
'src/tpm2_pytss/TCTILdr.py',
'src/tpm2_pytss/TCTISPIHelper.py',
'src/tpm2_pytss/TCTI.py',
'src/tpm2_pytss/constants.py',
'src/tpm2_pytss/fapi_info.py',
]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ dev =
myst-parser
build
installer
mypy
138 changes: 137 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Enum,
)
from textwrap import dedent
from cffi import cparser

# workaround bug https://github.com/pypa/pip/issues/7953
site.ENABLE_USER_SITE = "--user" in sys.argv[1:]
Expand Down Expand Up @@ -282,8 +283,143 @@ def run(self):
self.copy_file(vp, svp)


class type_hints_generator(type_generator):
is_int = set(("int",))
callbacks = dict()
functions = dict()

def build_function(self, d):
args = list()
for param in d.args.params:
pn = param.name
if pn is None:
# if the param doesn't have a name, ignore
continue
elif isinstance(
param.type,
(cparser.pycparser.c_ast.PtrDecl, cparser.pycparser.c_ast.ArrayDecl),
):
ft = "CData"
elif isinstance(
param.type, cparser.pycparser.c_ast.TypeDecl
) and isinstance(param.type.type, cparser.pycparser.c_ast.IdentifierType):
tn = param.type.type.names[0]
if tn in self.is_int:
ft = "int"
else:
raise ValueError(f"unable to handle C type {param.type}")
args.append((pn, ft))
if isinstance(d.type, cparser.pycparser.c_ast.TypeDecl) and isinstance(
d.type.type, cparser.pycparser.c_ast.IdentifierType
):
tn = d.type.type.names[0]
if tn in self.is_int:
rt = "int"
elif tn == "void":
rt = "None"
else:
rt = "CData"
elif isinstance(d.type, cparser.pycparser.c_ast.PtrDecl):
rt = "CData"
return (rt, tuple(args))

Check failure

Code scanning / CodeQL

Potentially uninitialized local variable Error

Local variable 'rt' may be used before it is initialized.

def write_type_hints(self, macros):
output = dedent(
"""# SPDX-License-Identifier: BSD-2
# This file is generated during the build process.
from typing import Callable
from .ffi import CData
# Defines
"""
)

# assume all defines are ints
for m in macros:
output += f"{m}: int\n"

output += "\n# Callback definitions\n"
for cname, callback in self.callbacks.items():
rt, args = callback
paramtypes = list()
for _, at in args:
paramtypes.append(at)
output += f"{cname}: Callable[[{', '.join(paramtypes)}], {rt}]\n"

output += "\n# Function definitions\n"
for fname, function in self.functions.items():
rt, args = function
params = list()
for an, at in args:
if an == "in":
an = "in_"
params.append(f"{an}: {at}")
output += f"def {fname}({', '.join(params)}) -> {rt}:...\n"

p = os.path.join(self.build_lib, "tpm2_pytss/_libtpm2_pytss/lib.pyi")
sp = os.path.join(
os.path.dirname(__file__), "src/tpm2_pytss/_libtpm2_pytss/lib.pyi"
)

if not self.dry_run:
self.mkpath(os.path.dirname(p))
with open(p, "wt") as tf:
tf.seek(0)
tf.truncate(0)
tf.write(output)

if self.inplace:
self.copy_file(p, sp)

def run(self):
super().run()

with open("libesys.h", "r") as sf:
cdata = sf.read()

parser = cparser.Parser()
ast, macros, _ = parser._parse(cdata)

for d in ast:
name = d.name
if isinstance(name, str) and name.startswith("__cffi_"):
# internal cffi stuff, ignore
continue
if isinstance(d, cparser.pycparser.c_ast.Typedef):
d = d.type
if isinstance(
d.type,
(
cparser.pycparser.c_ast.Struct,
cparser.pycparser.c_ast.Union,
cparser.pycparser.c_ast.Enum,
),
):
# ignore unions, structs and enums
pass
elif isinstance(d.type, cparser.pycparser.c_ast.IdentifierType):
# check if type is int, otherwise ignore
if len(d.type.names) == 0:
break
tn = d.type.names[0]
if tn in self.is_int:
self.is_int.add(name)
elif isinstance(d, cparser.pycparser.c_ast.PtrDecl) and isinstance(
d.type, cparser.pycparser.c_ast.FuncDecl
):
rt, args = self.build_function(d.type)
self.callbacks[name] = (rt, args)
elif isinstance(d, cparser.pycparser.c_ast.Decl) and isinstance(
d.type, cparser.pycparser.c_ast.FuncDecl
):
rt, args = self.build_function(d.type)
self.functions[name] = (rt, args)

self.write_type_hints(macros)


setup(
use_scm_version=True,
cffi_modules=["scripts/libtss2_build.py:ffibuilder"],
cmdclass={"build_ext": type_generator},
cmdclass={"build_ext": type_hints_generator},
)
20 changes: 12 additions & 8 deletions src/tpm2_pytss/TSS2_Exception.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from ._libtpm2_pytss import lib, ffi
from typing import Union
from typing import Union, TYPE_CHECKING


if TYPE_CHECKING:
from .constants import TSS2_RC, TPM2_RC


class TSS2_Exception(RuntimeError):
Expand All @@ -25,7 +29,7 @@ def __init__(self, rc: Union["TSS2_RC", "TPM2_RC", int]):
else:
self._error = self._rc

def _parse_fmt1(self):
def _parse_fmt1(self) -> None:
self._error = lib.TPM2_RC_FMT1 + (self.rc & 0x3F)

if self.rc & lib.TPM2_RC_P:
Expand All @@ -36,31 +40,31 @@ def _parse_fmt1(self):
self._handle = (self.rc & lib.TPM2_RC_N_MASK) >> 8

@property
def rc(self):
def rc(self) -> int:
"""int: The return code from the API call."""
return self._rc

@property
def handle(self):
def handle(self) -> int:
"""int: The handle related to the error, 0 if not related to any handle."""
return self._handle

@property
def parameter(self):
def parameter(self) -> int:
"""int: The parameter related to the error, 0 if not related to any parameter."""
return self._parameter

@property
def session(self):
def session(self) -> int:
"""int: The session related to the error, 0 if not related to any session."""
return self._session

@property
def error(self):
def error(self) -> int:
"""int: The error with handle, parameter and session stripped."""
return self._error

@property
def fmt1(self):
def fmt1(self) -> bool:
"""bool: True if the error is related to a handle, parameter or session """
return bool(self._rc & lib.TPM2_RC_FMT1)
8 changes: 4 additions & 4 deletions src/tpm2_pytss/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# if we can't, provide a better message.
try:
from ._libtpm2_pytss import lib
except ImportError as e:
parts = e.msg.split(": ", 2)
except ImportError as ie:
parts = ie.msg.split(": ", 2)
if len(parts) != 3:
raise e
raise ie
path, error, symbol = parts
if error != "undefined symbol":
raise e
raise ie
raise ImportError(
f"failed to load tpm2-tss bindigs in {path} due to missing symbol {symbol}, "
+ "ensure that you are using the same libraries the python module was built against."
Expand Down
17 changes: 17 additions & 0 deletions src/tpm2_pytss/_libtpm2_pytss/ffi.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Optional, Callable, Iterable

error: type[Exception]
class CData:
def __getitem__(self, index: int) -> "CData": ...

class CType:
kind: str
cname: str
item: "CType"
fields: Iterable[str]

NULL: CData
def gc(cdata: CData, destructor: Callable[[CData], None], size: int = 0)-> CData: ...
def typeof(cdata: CData) -> CType: ... # FIXME, support str
def new(cdecl: str, init: Optional[Callable[[CData], CData]] = None) -> CData: ...
def string(cdata: CData, maxlen: Optional[int] = None) -> bytes: ...
Loading

0 comments on commit 49f20a7

Please sign in to comment.