Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial mypy support #59

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

ahawker
Copy link

@ahawker ahawker commented Sep 8, 2020

Status: Ready for review
Issue: Fixes #57

If merged, this PR adds initial support for mypy.

  • Type stubs, .pyi files have been added for the package interface __init__.py and libusb1.py module.
  • The _version.py module and tests are currently excluded from checks (see mypy.ini). They're skipped now since they're "internal" but can easily be added to type checks in the future.

I did the best I could with the type definitions for functions that I'm not using in https://github.com/adbpy/transports but there are potentially some mistakes/inconsistencies. These should be relatively easily to "whack-o-mole" as they come up (or during code review for someone that knows the codebase better).

Testing

The stubs should pass normal and strict checks.

hawker@mbp:~/src/github.com/ahawker/python-libusb1|master⚡
⇒  mypy usb1 && mypy --strict usb1
Success: no issues found in 4 source files
Success: no issues found in 4 source files

There is also a new Travis CI job for validating the mypy type hints on py36.

I also ran them against all the code in the examples/ directory. All types exported from the package appear correct in there, with the only strict errors being the fact that the examples themselves aren't typed.

The _version.py and testUSB1 modules are both ignored by mypy
since they're "internal" to the package. They can be included in the
future by simply removing their 'ignore_errors' lines.
Copy link
Owner

@vpelletier vpelletier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which would be the better practice between having the types documenting/type-annotating the code as-is, or documenting/type-annotating only the the non-deprecated, user-relevant parts ?

Some examples of the latter:

  • USBTransferHelper.__init__ transfer argument could be hidden, along with the submit, cancel and isSubmitted methods
  • _ReleaseInterface could be an opaque object implementing the context manager protocol

Edit: and I failed at reviewing, I am not done yet but posting this comment submitted it. Oh well...

TransferType = int
TransferStatus = int
TransferCallback = Callable[['USBTransfer'], None]
ErrorCallback = Callable[[int], None]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The USBPollerThread.__init__ docstring is inconsistent with the code (...which is itself unnecessary complicated), this should rather be:

ErrorCallback = Callable[[USBError], None]

I fix the docstring and simplify the code.

TransferCallback = Callable[['USBTransfer'], None]
ErrorCallback = Callable[[int], None]
FileDescriptor = int
Events = Any
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While usb1 itself does not impose restrictions on this argument, it calls it internally with values coming from USBContext.getPollFDList, which come from libusb_pollfd.events, which is a c_short. So used poller need to at least support an integer value (an event mask, as in select.epoll.register & similar).

I believe this boils down to what the mypy best practice is for such case, and you are more familiar with it than I am.

UserData = object
Timeout = int
PollTimeout = Union[float, int, None]
TransferType = int
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity: is there a way to match these types (TransferType, TransferStatus, ...) with named constants ? So that a check error would happen if these are compared with something else than such named constant, even if it had the same integer value ? (ex: transfer.getType() == LIBUSB_TRANSFER_COMPLETED would fail, even though this test would be accidentally equivalent to ... == LIBUSB_TRANSFER_TYPE_CONTROL).

def isSubmited(self) -> bool: ...

class USBPollerThread(threading.Thread):
daemon: bool = ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this attribute declaration to be inherited from threading.Thread.

exc_callback: Optional[ErrorCallback] = ...) -> None: ...
def stop(self) -> None: ...
@staticmethod
def exceptionHandler(exc: BaseException) -> None: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be specialized a bit more, consistently with ErrorCallback: def exceptionHandler(exc: USBError) -> None: ...

Edit: BTW, I'm surprised that static analysis did not catch this type difference, as __init__ shadows this method with provided exc_callback which (as of this review) has a different type.

class USBPoller:
def __init__(self,
context: USBContext,
poller: USBPoller) -> None: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While USBPoller implements the API it expects from poller, other types are accepted (and in fact they should be the norm). Here I would expect a protocol rather than such stronger typing.

Maybe something like this (not tested):

PollEventMask = int
PollResult = List[Tuple[FileDescriptor, PollEventMask]]

class PollerType(Protocol):
  def register(self, fd: FileDescriptor, event_flags: PollEventMask) -> None: ...
  def unregister(self, fd: FileDescriptor) -> None: ...
  def poll(timeout: PollTimeout) -> PollResult: ...

# ...

class USBPoller:
    def __init__(self,
                 context: USBContext,
                 poller: PollerType) -> None: ...

Edit: And maybe class USBPoller(PollerType): if mypy recognises this as an implementation of that protocol, and lets it fill in the other methods, rather than having to duplicate their declaration (not obvious to me from mypy doc).

Copy link
Owner

@vpelletier vpelletier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more review checkpoint, I quickly skimmed over the first ~200 lines of libusb.pyi (the integer constants, I did not check that all names are present), and still have the functions to check.

So far, I've only caught a few mistakes, and most of my comments are open-ended: I'm not sure which way is best, and wondering about your opinion.

def getTransfer(self,
iso_packets: int = ...) -> USBTransfer: ...

class USBConfiguration:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__len__ is not specified here. I expected its type was inferred (as it's set to getNumInterfaces), but in further classes you specify it. I'm not sure which one is more correct.


class USBDevice:
device_p: libusb1.libusb_device_p_alias = ...
device_descriptor: Any = ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think I intended these properties as public API (because if one wants to access ctype objects, they should use usb1.libusb directly), so maybe they could be dropped (see also my overall question on what should/should not be typed). If kept, I think this could be changed to libusb1.libusb_device_descriptor type.

product_id: Any,
skip_on_access_error: bool = ...,
skip_on_error: bool = ...) -> Optional[USBDeviceHandle]: ...
def getPollFDList(self) -> Tuple[int, int]: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it should be: def getPollFDList(self) -> List[Tuple[int, int]]: ... (but I did not test)

added_cb: Optional[FDNotifierCallback] = ...,
removed_cb: Optional[FDNotifierCallback] = ...,
user_data: Optional[UserData] = ...) -> None: ...
def getNextTimeout(self) -> int: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly, this would produce a float, but I believe mypy is not that strict.

def getNextTimeout(self) -> int: ...
def setDebug(self,
level: int) -> None: ...
def tryLockEvents(self) -> int: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantically, this produces a boolean (returns 0 or 1). But type-wise, this is indeed strictly an integer. Not sure which one is best here.

def lockEvents(self) -> None: ...
def lockEventWaiters(self) -> None: ...
def waitForEvent(self,
tv: int = ...) -> None: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silmilarly to getNextTimeout, tv is actually a float.

def waitForEvent(self,
tv: int = ...) -> None: ...
def unlockEventWaiters(self) -> None: ...
def eventHandlingOK(self) -> int: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to tryLockEvents.

def eventHandlingOK(self) -> int: ...
def unlockEvents(self) -> None: ...
def handleEventsLocked(self) -> None: ...
def eventHandlerActive(self) -> int: ...
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to tryLockEvents.

CLASS_HID: int
CLASS_PHYSICAL: int
CLASS_PTP: int
CLASS_IMAGE = libusb1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo ?

@ahawker
Copy link
Author

ahawker commented Sep 14, 2020

@vpelletier Thank you for taking the the time to do an in-depth review & feedback! Life's gotten a bit busy so it'll be a little slower to tidy this up but just letting you know I haven't forgotten.

@vpelletier
Copy link
Owner

You're welcome. There is no hurry, on my side it's also getting pretty busy, and will probably remain so for about a month.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mypy Support?
2 participants