-
Notifications
You must be signed in to change notification settings - Fork 67
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
base: master
Are you sure you want to change the base?
Conversation
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.
There was a problem hiding this 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 thesubmit
,cancel
andisSubmitted
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] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 = ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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).
There was a problem hiding this 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: |
There was a problem hiding this comment.
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 = ... |
There was a problem hiding this comment.
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]: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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: ... |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo ?
@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. |
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. |
Status: Ready for review
Issue: Fixes #57
If merged, this PR adds initial support for
mypy
..pyi
files have been added for the package interface__init__.py
andlibusb1.py
module._version.py
module and tests are currently excluded from checks (seemypy.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.
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.