typeshed icon indicating copy to clipboard operation
typeshed copied to clipboard

Adding descriptor methods to _SimpleCData

Open DrInfiniteExplorer opened this issue 1 year ago • 8 comments

This allows adding type annotations to structures and unions that resolve to the underlying types when accessed inside structures.

I don't know how to make proper tests for this, so I've made some examples where I've marked the lines that are expected to fail typechecking.

import ctypes

class Annotated(ctypes.Structure):
    _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int), ("z", ctypes.c_int), ("f", ctypes.c_float)]
    x: ctypes.c_int
    y: ctypes.c_int
    z: ctypes.c_int
    f: ctypes.c_float

class NoAnnotation(ctypes.Structure):
    _fields_ = [("x", ctypes.c_int, "vec", Annotated)]

class NonCType:
    x : ctypes.c_int

ctypes.c_int().value += 2
ctypes.c_int() += 2          # FAIL

ctypes.c_float().value += 2
ctypes.c_float() += 2        # FAIL

# All passes; all access are Any
NoAnnotation().x += 2
NoAnnotation().vec.x += 2
NoAnnotation().noexist += 2

Annotated.x.value += 2
Annotated.x += 2             # FAIL
Annotated().x += 2
Annotated().x.value += 2     # FAIL
Annotated().f += 3.14
Annotated().f.value += 3.14  # FAIL

NonCType.x.value += 2
NonCType.x += 2              # FAIL
NonCType().x += 2            # FAIL
NonCType().x.value += 2

DrInfiniteExplorer avatar Jan 14 '24 07:01 DrInfiniteExplorer

Diff from mypy_primer, showing the effect of this PR on open source code:

comtypes (https://github.com/enthought/comtypes)
+ comtypes/typeinfo.py:391: error: Incompatible return value type (got "tuple[str, int | None]", expected "tuple[str, tagFUNCDESC | tagVARDESC | ITypeComp] | None")  [return-value]

github-actions[bot] avatar Jan 14 '24 07:01 github-actions[bot]

Diff from mypy_primer, showing the effect of this PR on open source code:

comtypes (https://github.com/enthought/comtypes)
+ comtypes/typeinfo.py:391: error: Incompatible return value type (got "tuple[str, int | None]", expected "tuple[str, tagFUNCDESC | tagVARDESC | ITypeComp] | None")  [return-value]

github-actions[bot] avatar Jan 14 '24 07:01 github-actions[bot]

I've checked out the problem from enthought/comtypes and these are my findings:

ITypeComp.Bind is specified to return Optional[Tuple[str, _UnionT[FUNCDESC, VARDESC, ITypeComp]]]. typeinfo.py:375.

The offending line returns "type", bindptr.lptcomp where bindptr is an instance of BINDPTR/tagBINDPTR. tagBindPtr.lptcomp is annotated as ITypeComp. typeinfo.py:646. The actual field type is POINTER(ITypeComp). typeinfo.p:L978

ITypeComp inherits from IUnknown which inherits from _IUnknown_Base which inherits from c_void_p. ITypeComp is effectively a c_void_p. When c_void_p is accessed as a member of Structure or Union the returned type is int | None.

When the interpreter returns bindptr.lptcomp, the type is POINTER(ITypeComp), and the returned type is ITypeComp, as defined by the _field_. When the typechecker sees the return, bindptr.lptcomp resolves as int, as instructed by the annotation. Thus the actual problem is that the annotation of the member doesn't match the type of the field declaration

edit: clarified last part, was tired when I wrote it, still am

DrInfiniteExplorer avatar Jan 14 '24 08:01 DrInfiniteExplorer

Now after the fact I saw #10595 . I think this proposal does what was intended in the PR and discussed in #10567, but better, as it is performed automatically by all primitives that are part of a structure/union and doesn't need manual type definitions as is done in that PR?

DrInfiniteExplorer avatar Jan 14 '24 09:01 DrInfiniteExplorer

See the test_cases/stdlib directory for examples on how to write tests. Basically, use typing_extensions.assert_type() to make sure a type is as expected, and # type: ignore for expected failures.

srittau avatar Jan 14 '24 11:01 srittau

Diff from mypy_primer, showing the effect of this PR on open source code:

comtypes (https://github.com/enthought/comtypes)
+ comtypes/typeinfo.py:391: error: Incompatible return value type (got "tuple[str, int | None]", expected "tuple[str, tagFUNCDESC | tagVARDESC | ITypeComp] | None")  [return-value]

github-actions[bot] avatar Jan 14 '24 21:01 github-actions[bot]

It turns out that subclassing the basic types causes them to not be unboxed, and this behaviour has always been part of ctypes. I'll try fix things to properly reflect this, and add more regression tests for the things added in #10595 so the type annotations added therein can be simplified.

I added # pyright: reportUninitializedInstanceVariable=false to the top of the new regression testing file since pyright complained about uninitialized members. Should _StructUnionBase or _StructUnionMeta be dataclassed? On one hand it'd solve that problem (if I understand dataclass correctly), and people already annotate ctypes structures. On the other hand, it'd sort of falsely convey that the class is defined by the annotations

DrInfiniteExplorer avatar Jan 14 '24 22:01 DrInfiniteExplorer

Diff from mypy_primer, showing the effect of this PR on open source code:

comtypes (https://github.com/enthought/comtypes)
+ comtypes/typeinfo.py:452: error: Incompatible return value type (got "tuple[str, int | None]", expected "tuple[str, tagFUNCDESC | tagVARDESC | ITypeComp] | None")  [return-value]

github-actions[bot] avatar Feb 05 '24 19:02 github-actions[bot]