Search code examples
pythonctypesmypyioctl

VSCode MyPy error: "ioctl" has incompatible type "my_struct"; expected "Union[int, str]"


I have the following python snippet that is generating MyPy "problems" (in vscode).

my_struct = MyStruct()    
#! set mutable flag to true to place data in our object.
fcntl.ioctl( dev_hand.fileno(), my_ioctl_id, my_struct, True )

The error is:

Argument 3 to "ioctl" has incompatible type "my_struct"; expected "Union[int, str]"

MyStruct is a ctypes structure. All the examples for using ioctl() with ctypes structures show passing the instance to ioctl(). Indeed this does work, except now MyPy is complaining.

I'd prefer not to convert to bytes and manually pack/unpack with the struct module (which I presume is one solution).

I'm using Python 3.7.3 on Linux (Debian Buster), with mypy 0.782

Thanks, Brendan.


NOTE: I forgot to mention that my code is targeting Python 2.7, as it is legacy from a Debian Jessie target system. I am using the --py2 switch for mypy (which must run on Python 3).

The ioctl() function has the following signature, which seems to come from the vscode server (remote ssh) ms-python .... typeshed/stdlib/3/fcntl.pyi`

def ioctl(fd: _AnyFile,
          request: int,
          arg: Union[int, bytes] = ...,
          mutate_flag: bool = ...) -> Any: ...

Here is a more complete code example.

from typing import ( BinaryIO, )

import ioctl
import fcntl

from ctypes import ( c_uint32, Structure, addressof )

class Point ( Structure ) :
    _fields_ = [ ( 'x', c_uint32 ), ( 'y', c_uint32 ) ]

def ioctl_get_point (
        dev_hand,
        ) :
    point = Point()
    fcntl.ioctl( dev_hand, 0x12345678, point, True )   #! ** MyPy does NOT complain at all **

def ioctl_get_point_2 (
        dev_hand,               # type: BinaryIO
        ) :
    point = Point()
    fcntl.ioctl( dev_hand, 0x12345678, point, True )   #! ** MyPy complains about arg 3 **
    return point

def ioctl_get_point_3 (
        dev_hand,
        ) :                     # type: (...) -> Point
    point = Point()
    fcntl.ioctl( dev_hand, 0x12345678, point, True )   #! ** MyPy complains about arg 3 **
    return point

def ioctl_get_point_4 (
        dev_hand,               # type: BinaryIO
        ) :                     # type: (...) -> Point
    point = Point()
    fcntl.ioctl( dev_hand, 0x12345678, point, True )   #! ** MyPy complains about arg 3 **
    return point

def ioctl_get_point_5 (
        dev_hand,               # type: BinaryIO
        ) :                     # type: (...) -> Point
    point = Point()
    fcntl.ioctl( dev_hand, 0x12345678, addressof( point ), True )   #! ** MyPy does NOT complain at all **
    return point

To me, it seems like using the ctypes.addressof() function, that @CristiFati suggested, is the simplest solution.

Unfortunately that doesn't work. The ioctl() function needs to know the size of the object.

Thanks, Brendan.


Solution

  • mypy follows the specs of the fnctl.ioctl function here:

    The parameter arg can be one of an integer, an object supporting the read-only buffer interface (like bytes) or an object supporting the read-write buffer interface (like bytearray).

    The complaint is thus a legitimate one.

    I'd prefer not to convert to bytes and manually pack/unpack with the struct module

    With the help of the TYPE_CHECKING constant, you can introduce a local stub with a type hint for fnctl.ioctl that will override stdlib's type hint:

    import ctypes
    from typing import TYPE_CHECKING
    
    
    class MyStruct(ctypes.Structure):
        _fields_ = [...]
    
    
    if TYPE_CHECKING:  # this is only processed by mypy
        from typing import Protocol, Union, TypeVar
    
        class HasFileno(Protocol):
            def fileno(self) -> int: ...
    
        FileDescriptorLike = Union[int, HasFileno]
    
        _S = TypeVar('_S', bound=ctypes.Structure)
    
        def ioctl(__fd: FileDescriptorLike, __request: int, __arg: Union[int, bytes, _S] = ..., __mutate_flag: bool = ...) -> int: ...
    
    else:  # this will be executed at runtime and ignored by mypy
        from fcntl import ioctl
    
    
    my_struct = MyStruct(...)
    my_ioctl_id = ...
    dev_hand = ...
    
    ioctl(dev_hand.fileno(), my_ioctl_id, my_struct, True)  # mypy won't complain here anymore